From e5a13dd73822418b7df32da69c13b679b533fa4b Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 30 Mar 2026 19:33:50 +0000 Subject: [PATCH 01/41] feat: Schema-based RestXML serialization/deserialization - Add XML traits (XmlName, XmlFlattened, XmlAttribute, XmlNamespace) to Smithy module - Create SmithyRestXML module with Serializer, Deserializer, Codec, HTTPClientProtocol, Plugin, BaseError - Add SmithyRestXMLTypes.kt and RestXMLPlugin.kt for Kotlin codegen - Update SerdeUtils to include RestXML in useSchemaBased() - Update RestXmlCustomizations with renderClientProtocol and plugins - Update RestXmlProtocolGenerator to remove schema-based middlewares --- Package.swift | 11 + .../TraitLibrary/AllSupportedTraits.swift | 4 + .../TraitLibrary/XmlAttributeTrait.swift | 12 + .../TraitLibrary/XmlFlattenedTrait.swift | 12 + .../Smithy/TraitLibrary/XmlNameTrait.swift | 19 ++ .../TraitLibrary/XmlNamespaceTrait.swift | 32 +++ Sources/SmithyRestXML/BaseError.swift | 71 +++++ Sources/SmithyRestXML/Codec.swift | 24 ++ Sources/SmithyRestXML/Deserializer.swift | 243 ++++++++++++++++++ .../SmithyRestXML/HTTPClientProtocol.swift | 131 ++++++++++ Sources/SmithyRestXML/Plugin.swift | 23 ++ Sources/SmithyRestXML/Serializer.swift | 209 +++++++++++++++ Sources/SmithyRestXML/TimestampUtils.swift | 21 ++ .../smithy/swift/codegen/SwiftDependency.kt | 1 + .../aws/protocols/restxml/RestXMLPlugin.kt | 9 + .../restxml/RestXmlCustomizations.kt | 8 + .../restxml/RestXmlProtocolGenerator.kt | 12 + .../codegen/integration/serde/SerdeUtils.kt | 8 +- .../swiftmodules/SmithyRestXMLTypes.kt | 24 ++ 19 files changed, 872 insertions(+), 2 deletions(-) create mode 100644 Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/XmlNameTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift create mode 100644 Sources/SmithyRestXML/BaseError.swift create mode 100644 Sources/SmithyRestXML/Codec.swift create mode 100644 Sources/SmithyRestXML/Deserializer.swift create mode 100644 Sources/SmithyRestXML/HTTPClientProtocol.swift create mode 100644 Sources/SmithyRestXML/Plugin.swift create mode 100644 Sources/SmithyRestXML/Serializer.swift create mode 100644 Sources/SmithyRestXML/TimestampUtils.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXMLPlugin.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt diff --git a/Package.swift b/Package.swift index 0d5219d01..8600212ed 100644 --- a/Package.swift +++ b/Package.swift @@ -32,6 +32,7 @@ let package = Package( .library(name: "Smithy", targets: ["Smithy"]), .library(name: "SmithySerialization", targets: ["SmithySerialization"]), .library(name: "SmithyRPCv2CBOR", targets: ["SmithyRPCv2CBOR"]), + .library(name: "SmithyRestXML", targets: ["SmithyRestXML"]), .library(name: "ClientRuntime", targets: ["ClientRuntime"]), .library(name: "SmithyRetriesAPI", targets: ["SmithyRetriesAPI"]), .library(name: "SmithyRetries", targets: ["SmithyRetries"]), @@ -305,6 +306,16 @@ let package = Package( "SmithyCBOR", ] ), + .target( + name: "SmithyRestXML", + dependencies: [ + "ClientRuntime", + "Smithy", + "SmithySerialization", + "SmithyXML", + "SmithyTimestamps", + ] + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift index be47d0825..cf3a7d88b 100644 --- a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -31,6 +31,10 @@ private let allSupportedTraitTypes: [ShapeID: any Trait.Type] = [ SparseTrait.id: SparseTrait.self, TimestampFormatTrait.id: TimestampFormatTrait.self, UnitTypeTrait.id: UnitTypeTrait.self, // UnitTypeTrait will only ever appear in Prelude.unitSchema + XmlAttributeTrait.id: XmlAttributeTrait.self, + XmlFlattenedTrait.id: XmlFlattenedTrait.self, + XmlNameTrait.id: XmlNameTrait.self, + XmlNamespaceTrait.id: XmlNamespaceTrait.self, // Synthetic traits TargetsUnitTrait.id: TargetsUnitTrait.self, diff --git a/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift b/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift new file mode 100644 index 000000000..f246cf002 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XmlAttributeTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "xmlAttribute") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift b/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift new file mode 100644 index 000000000..a3cc73279 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XmlFlattenedTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "xmlFlattened") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/XmlNameTrait.swift b/Sources/Smithy/TraitLibrary/XmlNameTrait.swift new file mode 100644 index 000000000..ead35a7e9 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/XmlNameTrait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XmlNameTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "xmlName") } + public let value: String + public var node: Node { .string(value) } + + public init(node: Node) throws { + guard case .string(let value) = node else { + throw TraitError("xmlName trait requires a string value") + } + self.value = value + } +} diff --git a/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift b/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift new file mode 100644 index 000000000..97cea412c --- /dev/null +++ b/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XmlNamespaceTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "xmlNamespace") } + public let uri: String + public let prefix: String? + public var node: Node { + var dict: [String: Node] = ["uri": .string(uri)] + if let prefix { dict["prefix"] = .string(prefix) } + return .object(dict) + } + + public init(node: Node) throws { + guard case .object(let dict) = node else { + throw TraitError("xmlNamespace trait requires an object value") + } + guard case .string(let uri) = dict["uri"] else { + throw TraitError("xmlNamespace trait requires a 'uri' string") + } + self.uri = uri + if case .string(let prefix) = dict["prefix"] { + self.prefix = prefix + } else { + self.prefix = nil + } + } +} diff --git a/Sources/SmithyRestXML/BaseError.swift b/Sources/SmithyRestXML/BaseError.swift new file mode 100644 index 000000000..6f5d03a48 --- /dev/null +++ b/Sources/SmithyRestXML/BaseError.swift @@ -0,0 +1,71 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.Schema +import protocol SmithySerialization.DeserializableStruct +import typealias SmithySerialization.ReadStructConsumer +import protocol SmithySerialization.ShapeDeserializer + +@_spi(RestXML) +public struct BaseError { + public var code: String? + public var message: String? + public var requestId: String? +} + +extension BaseError: DeserializableStruct { + + /// Schema for the `` wrapper element in RestXML error responses. + private static var errorSchema: Smithy.Schema { + .init( + id: .init("swift.synthetic", "RestXMLBaseError"), + type: .structure, + members: [ + .init( + id: .init("swift.synthetic", "RestXMLBaseError", "Code"), + type: .member, + target: Prelude.stringSchema, + index: 0 + ), + .init( + id: .init("swift.synthetic", "RestXMLBaseError", "Message"), + type: .member, + target: Prelude.stringSchema, + index: 1 + ), + .init( + id: .init("swift.synthetic", "RestXMLBaseError", "RequestId"), + type: .member, + target: Prelude.stringSchema, + index: 2 + ), + ] + ) + } + + public static var readConsumer: SmithySerialization.ReadStructConsumer { + { memberSchema, value, deserializer in + switch memberSchema.index { + case 0: + value.code = try deserializer.readString(memberSchema) + case 1: + value.message = try deserializer.readString(memberSchema) + case 2: + value.requestId = try deserializer.readString(memberSchema) + default: + break + } + } + } + + public static func deserialize(_ deserializer: any ShapeDeserializer) throws -> Self { + var value = Self() + try deserializer.readStruct(Self.errorSchema, &value) + return value + } +} diff --git a/Sources/SmithyRestXML/Codec.swift b/Sources/SmithyRestXML/Codec.swift new file mode 100644 index 000000000..d077cc957 --- /dev/null +++ b/Sources/SmithyRestXML/Codec.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import protocol SmithySerialization.Codec +import protocol SmithySerialization.ShapeDeserializer +import protocol SmithySerialization.ShapeSerializer + +public struct Codec: SmithySerialization.Codec { + + public init() {} + + public func makeSerializer() throws -> any ShapeSerializer { + Serializer() + } + + public func makeDeserializer(data: Data) throws -> any ShapeDeserializer { + try Deserializer(data: data) + } +} diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift new file mode 100644 index 000000000..f2ab15c5b --- /dev/null +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -0,0 +1,243 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import struct Smithy.Document +import struct Smithy.Schema +import struct Smithy.TimestampFormatTrait +import struct Smithy.XmlAttributeTrait +import struct Smithy.XmlFlattenedTrait +import struct Smithy.XmlNameTrait +import protocol SmithySerialization.DeserializableStruct +import typealias SmithySerialization.ReadStructConsumer +import typealias SmithySerialization.ReadValueConsumer +import struct SmithySerialization.SerializerError +import protocol SmithySerialization.ShapeDeserializer +@_spi(SmithyReadWrite) import class SmithyXML.Reader +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo +@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter +@_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat + +public struct Deserializer: ShapeDeserializer { + let reader: Reader + + public init(data: Data) throws { + if data.isEmpty { + self.reader = try Reader.from(data: Data("".utf8)) + // Mark as empty by using a sentinel + } else { + self.reader = try Reader.from(data: data) + } + } + + init(reader: Reader) { + self.reader = reader + } + + private func targetSchema(_ schema: Schema) -> Schema { + schema.target ?? schema + } + + // MARK: - ShapeDeserializer + + public func readStruct(_ schema: Schema, _ value: inout T) throws { + let structSchema: Schema + switch schema.type { + case .structure, .union: + structSchema = schema + case .member: + guard let target = schema.target else { + throw XMLDeserializerError("Expected non-nil target on \(schema)") + } + structSchema = target + default: + throw XMLDeserializerError("unexpected schema type \(schema.type) used with readStruct") + } + + for member in structSchema.members { + let elementName = xmlElementName(for: member) + let isAttribute = member.hasTrait(XmlAttributeTrait.self) + let childReader: Reader + if isAttribute { + childReader = reader[NodeInfo(elementName, location: .attribute)] + } else { + childReader = reader[NodeInfo(elementName)] + } + guard childReader.hasContent || !childReader.children.isEmpty else { continue } + do { + let memberDeserializer = Deserializer(reader: childReader) + try T.readConsumer(member, &value, memberDeserializer) + } catch is DecodedNull { + // skip null + } + } + } + + public func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] { + let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) + var list = [E]() + + if isFlattened { + let elementName = xmlElementName(for: schema) + let siblings = (reader.parent?.children ?? [reader]).filter { + $0.nodeInfo.name == elementName + } + guard !siblings.isEmpty else { return list } + for sibling in siblings { + let element = try consumer(Deserializer(reader: sibling)) + list.append(element) + } + } else { + let memberSchema = targetSchema(schema).member + let memberName = xmlElementName(for: memberSchema) + let members = reader.children.filter { $0.nodeInfo.name == memberName } + for member in members { + let element = try consumer(Deserializer(reader: member)) + list.append(element) + } + } + return list + } + + public func readMap(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [String: V] { + let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) + let mapSchema = targetSchema(schema) + let keyName = xmlElementName(for: mapSchema.key) + let valueName = xmlElementName(for: mapSchema.value) + var map = [String: V]() + + let entries: [Reader] + if isFlattened { + let elementName = xmlElementName(for: schema) + entries = (reader.parent?.children ?? []).filter { $0.nodeInfo.name == elementName } + } else { + entries = reader.children.filter { $0.nodeInfo.name == "entry" } + } + + for entry in entries { + let keyReader = entry[NodeInfo(keyName)] + guard let key: String = try keyReader.readIfPresent() else { continue } + let valueReader = entry[NodeInfo(valueName)] + let value = try consumer(Deserializer(reader: valueReader)) + map[key] = value + } + return map + } + + public func readBoolean(_ schema: Schema) throws -> Bool { + guard let value: Bool = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected boolean for \(schema.id)") + } + return value + } + + public func readByte(_ schema: Schema) throws -> Int8 { + guard let value: Int8 = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Int8 for \(schema.id)") + } + return value + } + + public func readShort(_ schema: Schema) throws -> Int16 { + guard let value: Int16 = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Int16 for \(schema.id)") + } + return value + } + + public func readInteger(_ schema: Schema) throws -> Int { + guard let value: Int = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Int for \(schema.id)") + } + return value + } + + public func readLong(_ schema: Schema) throws -> Int { + guard let value: Int = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Int (long) for \(schema.id)") + } + return value + } + + public func readFloat(_ schema: Schema) throws -> Float { + guard let value: Float = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Float for \(schema.id)") + } + return value + } + + public func readDouble(_ schema: Schema) throws -> Double { + guard let value: Double = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected Double for \(schema.id)") + } + return value + } + + public func readBigInteger(_ schema: Schema) throws -> Int64 { + // Reader doesn't have Int64 readIfPresent, read as String and convert + guard let str: String = try reader.readIfPresent(), let value = Int64(str) else { + throw XMLDeserializerError("Expected Int64 for \(schema.id)") + } + return value + } + + public func readBigDecimal(_ schema: Schema) throws -> Double { + try readDouble(schema) + } + + public func readString(_ schema: Schema) throws -> String { + guard let value: String = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected String for \(schema.id)") + } + return value + } + + public func readBlob(_ schema: Schema) throws -> Data { + guard let value: Data = try reader.readIfPresent() else { + throw XMLDeserializerError("Expected blob for \(schema.id)") + } + return value + } + + public func readTimestamp(_ schema: Schema) throws -> Date { + let format = resolveTimestampFormat(schema) + guard let value: Date = try reader.readTimestampIfPresent(format: format) else { + throw XMLDeserializerError("Expected timestamp for \(schema.id)") + } + return value + } + + public func readDocument(_ schema: Schema) throws -> Document { + throw SerializerError("Document type not supported in XML") + } + + public func readNull(_ schema: Schema) throws -> T? { + return nil + } + + public func isNull() throws -> Bool { + !reader.hasContent && reader.children.isEmpty + } + + public var containerSize: Int { reader.children.count } + + // MARK: - Private + + private func xmlElementName(for schema: Schema) -> String { + (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.name + } +} + +struct XMLDeserializerError: Error { + let localizedDescription: String + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} + +struct DecodedNull: Error {} diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift new file mode 100644 index 000000000..d27a4fc8a --- /dev/null +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -0,0 +1,131 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import protocol ClientRuntime.HTTPError +import protocol ClientRuntime.ServiceError +import struct ClientRuntime.UnknownHTTPServiceError +import struct Foundation.Data +import enum Smithy.ByteStream +import enum Smithy.ClientError +import class Smithy.Context +import struct Smithy.Schema +import struct Smithy.ShapeID +import struct Smithy.TargetsUnitTrait +import struct Smithy.XmlNameTrait +import class SmithyHTTPAPI.HTTPRequest +import class SmithyHTTPAPI.HTTPRequestBuilder +import class SmithyHTTPAPI.HTTPResponse +@_spi(SmithyReadWrite) import class SmithyXML.Reader +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo +import protocol SmithySerialization.ClientProtocol +import protocol SmithySerialization.Codec +import protocol SmithySerialization.DeserializableStruct +import struct SmithySerialization.Operation +import struct SmithySerialization.TypeRegistry + +public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { + public typealias RequestType = HTTPRequest + public typealias ResponseType = HTTPResponse + + public let id = ShapeID("aws.protocols", "restXml") + public let codec: SmithySerialization.Codec = Codec() + public let noErrorWrapping: Bool + + public init(noErrorWrapping: Bool = false) { + self.noErrorWrapping = noErrorWrapping + } + + public func serializeRequest( + operation: Operation, + input: Input, + requestBuilder: HTTPRequestBuilder, + context: Context + ) throws { + // If the operation input targets smithy.api#Unit, don't serialize a body. + guard !operation.inputSchema.hasTrait(TargetsUnitTrait.self) else { + requestBuilder.withBody(.data(nil)) + return + } + + let serializer = try codec.makeSerializer() + try input.serialize(serializer) + let data = try serializer.data + let body = ByteStream.data(data) + requestBuilder.withBody(body) + } + + public func deserializeResponse( + operation: Operation, + context: Context, + response: HTTPResponse + ) async throws -> Output { + let bodyData = try await response.body.readData() ?? Data() + if (200..<300).contains(response.statusCode.rawValue) { + let deserializer = try codec.makeDeserializer(data: bodyData) + return try Output.deserialize(deserializer) + } else { + let errorTypeRegistry = operation.errorTypeRegistry + + // Parse the error response. RestXML errors are wrapped in ... + // unless noErrorWrapping is true. + let errorDeserializer = try Deserializer(data: bodyData) + let errorReader = errorDeserializer.reader + + // Navigate to the element if wrapping is present + let baseErrorDeserializer: Deserializer + if noErrorWrapping { + baseErrorDeserializer = errorDeserializer + } else { + let errorElement = errorReader.children.first { + $0.nodeInfo.name == "Error" + } ?? errorReader + baseErrorDeserializer = Deserializer(reader: errorElement) + } + + let baseError = try BaseError.deserialize(baseErrorDeserializer) + let code = baseError.code + + let registryEntry: TypeRegistry.Entry? + if let code { + registryEntry = errorTypeRegistry.find { entry in + entry.schema.id.name == code + } + } else { + registryEntry = nil + } + + if let registryEntry { + // Re-deserialize the error body into the specific error type + let specificDeserializer: Deserializer + if noErrorWrapping { + specificDeserializer = errorDeserializer + } else { + let errorElement = errorReader.children.first { + $0.nodeInfo.name == "Error" + } ?? errorReader + specificDeserializer = Deserializer(reader: errorElement) + } + let error = try registryEntry.swiftType.deserialize(specificDeserializer) + + guard var modeledError = error as? ServiceError & HTTPError & Error else { + throw ClientError.invalidValue( + "Modeled error does not conform to ServiceError & HTTPError & Error." + ) + } + modeledError.message = baseError.message + modeledError.httpResponse = response + throw modeledError + } else { + throw UnknownHTTPServiceError( + httpResponse: response, + message: baseError.message, + typeName: code + ) + } + } + } +} diff --git a/Sources/SmithyRestXML/Plugin.swift b/Sources/SmithyRestXML/Plugin.swift new file mode 100644 index 000000000..6d644469c --- /dev/null +++ b/Sources/SmithyRestXML/Plugin.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ClientRuntime +import Smithy +import class SmithyHTTPAPI.HTTPRequest +import class SmithyHTTPAPI.HTTPResponse +import protocol SmithySerialization.DeserializableStruct +import protocol SmithySerialization.SerializableStruct + +public struct Plugin: ClientRuntime.Plugin { + + public init() {} + + public func configureClient(clientConfiguration: inout Config) async throws { + // RestXML plugin currently has no additional configuration to apply. + // URL path and other HTTP bindings are handled by code-generated middlewares. + } +} diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift new file mode 100644 index 000000000..da875b7e4 --- /dev/null +++ b/Sources/SmithyRestXML/Serializer.swift @@ -0,0 +1,209 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import struct Smithy.Schema +import struct Smithy.TimestampFormatTrait +import struct Smithy.XmlAttributeTrait +import struct Smithy.XmlFlattenedTrait +import struct Smithy.XmlNameTrait +import struct Smithy.XmlNamespaceTrait +import protocol Smithy.SmithyDocument +import protocol SmithySerialization.SerializableStruct +import struct SmithySerialization.SerializerError +import protocol SmithySerialization.ShapeSerializer +import typealias SmithySerialization.WriteValueConsumer +@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter +@_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat + +public final class Serializer: ShapeSerializer { + private var xmlParts: [String] = [] + + public init() {} + + // MARK: - ShapeSerializer + + public func writeStruct(_ schema: Schema, _ value: S) throws { + let elementName = xmlElementName(for: schema) + let nsAttr = xmlNamespaceAttr(for: schema) + xmlParts.append("<\(elementName)\(nsAttr)>") + for member in schema.members { + try S.writeConsumer(member, value, self) + } + xmlParts.append("") + } + + public func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws { + let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) + if isFlattened { + let elementName = xmlElementName(for: schema) + for element in value { + xmlParts.append("<\(elementName)>") + try consumer(element, self) + xmlParts.append("") + } + } else { + writeMemberOpen(schema: schema) + let memberSchema = (schema.target ?? schema).member + let memberName = xmlElementName(for: memberSchema) + for element in value { + xmlParts.append("<\(memberName)>") + try consumer(element, self) + xmlParts.append("") + } + writeMemberClose(schema: schema) + } + } + + public func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws { + let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) + let mapSchema = schema.target ?? schema + let keyName = xmlElementName(for: mapSchema.key) + let valueName = xmlElementName(for: mapSchema.value) + if isFlattened { + let elementName = xmlElementName(for: schema) + for (k, v) in value { + xmlParts.append("<\(elementName)>") + xmlParts.append("<\(keyName)>\(xmlEscape(k))") + xmlParts.append("<\(valueName)>") + try consumer(v, self) + xmlParts.append("") + xmlParts.append("") + } + } else { + writeMemberOpen(schema: schema) + for (k, v) in value { + xmlParts.append("") + xmlParts.append("<\(keyName)>\(xmlEscape(k))") + xmlParts.append("<\(valueName)>") + try consumer(v, self) + xmlParts.append("") + xmlParts.append("") + } + writeMemberClose(schema: schema) + } + } + + public func writeBoolean(_ schema: Schema, _ value: Bool) throws { + writeScalar(schema: schema, string: value ? "true" : "false") + } + + public func writeByte(_ schema: Schema, _ value: Int8) throws { + writeScalar(schema: schema, string: "\(value)") + } + + public func writeShort(_ schema: Schema, _ value: Int16) throws { + writeScalar(schema: schema, string: "\(value)") + } + + public func writeInteger(_ schema: Schema, _ value: Int) throws { + writeScalar(schema: schema, string: "\(value)") + } + + public func writeLong(_ schema: Schema, _ value: Int) throws { + writeScalar(schema: schema, string: "\(value)") + } + + public func writeFloat(_ schema: Schema, _ value: Float) throws { + if value.isNaN { writeScalar(schema: schema, string: "NaN") } + else if value == .infinity { writeScalar(schema: schema, string: "Infinity") } + else if value == -.infinity { writeScalar(schema: schema, string: "-Infinity") } + else { writeScalar(schema: schema, string: "\(value)") } + } + + public func writeDouble(_ schema: Schema, _ value: Double) throws { + if value.isNaN { writeScalar(schema: schema, string: "NaN") } + else if value == .infinity { writeScalar(schema: schema, string: "Infinity") } + else if value == -.infinity { writeScalar(schema: schema, string: "-Infinity") } + else { writeScalar(schema: schema, string: "\(value)") } + } + + public func writeBigInteger(_ schema: Schema, _ value: Int64) throws { + writeScalar(schema: schema, string: "\(value)") + } + + public func writeBigDecimal(_ schema: Schema, _ value: Double) throws { + try writeDouble(schema, value) + } + + public func writeString(_ schema: Schema, _ value: String) throws { + writeScalar(schema: schema, string: xmlEscape(value)) + } + + public func writeBlob(_ schema: Schema, _ value: Data) throws { + writeScalar(schema: schema, string: value.base64EncodedString()) + } + + public func writeTimestamp(_ schema: Schema, _ value: Date) throws { + let tsFormat = resolveTimestampFormat(schema) + let formatted = TimestampFormatter(format: tsFormat).string(from: value) + writeScalar(schema: schema, string: formatted) + } + + public func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + throw SerializerError("Document type not supported in XML") + } + + public func writeNull(_ schema: Schema) throws { + // Null not defined in XML. No operation. + } + + public var data: Data { + Data(xmlParts.joined().utf8) + } + + // MARK: - Private + + private func xmlElementName(for schema: Schema) -> String { + (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.name + } + + private func xmlNamespaceAttr(for schema: Schema) -> String { + guard let ns = try? schema.getTrait(XmlNamespaceTrait.self) else { return "" } + if let prefix = ns.prefix, !prefix.isEmpty { + return " xmlns:\(prefix)=\"\(ns.uri)\"" + } + return " xmlns=\"\(ns.uri)\"" + } + + private func writeScalar(schema: Schema, string: String) { + if schema.hasTrait(XmlAttributeTrait.self) { + // Attributes are handled during struct writing; this is a fallback + writeMemberOpen(schema: schema) + xmlParts.append(string) + writeMemberClose(schema: schema) + } else { + writeMemberOpen(schema: schema) + xmlParts.append(string) + writeMemberClose(schema: schema) + } + } + + private func writeMemberOpen(schema: Schema) { + guard let name = schema.memberName else { return } + let elementName = (try? schema.getTrait(XmlNameTrait.self))?.value ?? name + let nsAttr = xmlNamespaceAttr(for: schema) + xmlParts.append("<\(elementName)\(nsAttr)>") + } + + private func writeMemberClose(schema: Schema) { + guard let name = schema.memberName else { return } + let elementName = (try? schema.getTrait(XmlNameTrait.self))?.value ?? name + xmlParts.append("") + } + + private func xmlEscape(_ string: String) -> String { + string + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) + .replacingOccurrences(of: "'", with: "'") + } +} diff --git a/Sources/SmithyRestXML/TimestampUtils.swift b/Sources/SmithyRestXML/TimestampUtils.swift new file mode 100644 index 000000000..535da1f50 --- /dev/null +++ b/Sources/SmithyRestXML/TimestampUtils.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema +import struct Smithy.TimestampFormatTrait +@_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat + +func resolveTimestampFormat(_ schema: Schema) -> TimestampFormat { + guard let traitFormat = try? schema.getTrait(TimestampFormatTrait.self)?.format else { + return .dateTime // XML default + } + switch traitFormat { + case .dateTime: return .dateTime + case .httpDate: return .httpDate + case .epochSeconds: return .epochSeconds + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index 8d7186d6a..e1fb9f103 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -65,6 +65,7 @@ class SwiftDependency( val SMITHY_CBOR = smithySwiftDependency("SmithyCBOR") val SMITHY_AWSJSON = smithySwiftDependency("SmithyAWSJSON") val SMITHY_RPCV2CBOR = smithySwiftDependency("SmithyRPCv2CBOR") + val SMITHY_REST_XML = smithySwiftDependency("SmithyRestXML") fun smithySwiftDependency(name: String): SwiftDependency = SwiftDependency( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXMLPlugin.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXMLPlugin.kt new file mode 100644 index 000000000..1ed332c9d --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXMLPlugin.kt @@ -0,0 +1,9 @@ +package software.amazon.smithy.swift.codegen.aws.protocols.restxml + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.swift.codegen.integration.Plugin +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyRestXMLTypes + +class RestXMLPlugin : Plugin { + override val className: Symbol = SmithyRestXMLTypes.Plugin +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt index 7765c89c4..ff7cdb769 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt @@ -6,9 +6,17 @@ package software.amazon.smithy.swift.codegen.aws.protocols.restxml import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.DefaultHTTPProtocolCustomizations +import software.amazon.smithy.swift.codegen.integration.Plugin import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyRestXMLTypes open class RestXmlCustomizations : DefaultHTTPProtocolCustomizations() { override val baseErrorSymbol: Symbol = ClientRuntimeTypes.RestXML.RestXMLError + + override fun renderClientProtocol(writer: SwiftWriter): String = + writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) + + override val plugins: List = listOf(RestXMLPlugin()) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlProtocolGenerator.kt index a556f5f28..ffe246fc5 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlProtocolGenerator.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.swift.codegen.aws.protocols.restxml import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.codegen.core.Symbol 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.ShapeId import software.amazon.smithy.swift.codegen.integration.DefaultHTTPProtocolCustomizations @@ -49,6 +50,17 @@ open class RestXmlProtocolGenerator( "S3PreservesLeadingDotSegmentInUriLabel", ) + override fun addProtocolSpecificMiddleware( + ctx: ProtocolGenerator.GenerationContext, + operation: OperationShape, + ) { + super.addProtocolSpecificMiddleware(ctx, operation) + + // Remove these middlewares as they are handled by the schema-based ClientProtocol & Operation + operationMiddleware.removeMiddleware(operation, "OperationInputBodyMiddleware") + operationMiddleware.removeMiddleware(operation, "DeserializeMiddleware") + } + override fun generateDeserializers(ctx: ProtocolGenerator.GenerationContext) { super.generateDeserializers(ctx) val errorShapes = resolveErrorShapes(ctx) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt index 50a07212a..8e92c2850 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt @@ -1,5 +1,7 @@ package software.amazon.smithy.swift.codegen.integration.serde +import software.amazon.smithy.aws.traits.protocols.RestXmlTrait +import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.protocol.traits.Rpcv2CborTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.model.hasTrait @@ -7,8 +9,10 @@ import software.amazon.smithy.swift.codegen.model.hasTrait class SerdeUtils { companion object { fun useSchemaBased(ctx: ProtocolGenerator.GenerationContext) = + useSchemaBased(ctx.service) + + fun useSchemaBased(service: ServiceShape) = // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based - // Right now this function only returns true for rpcv2Cbor based services - ctx.service.hasTrait() + service.hasTrait() || service.hasTrait() } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt new file mode 100644 index 000000000..ab358703b --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt @@ -0,0 +1,24 @@ +package software.amazon.smithy.swift.codegen.swiftmodules + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.swift.codegen.SwiftDeclaration +import software.amazon.smithy.swift.codegen.SwiftDependency + +object SmithyRestXMLTypes { + val HTTPClientProtocol = runtimeSymbol("HTTPClientProtocol", SwiftDeclaration.STRUCT) + val Plugin = runtimeSymbol("Plugin", SwiftDeclaration.STRUCT) +} + +private fun runtimeSymbol( + name: String, + declaration: SwiftDeclaration?, + additionalImports: List = emptyList(), + spiName: List = emptyList(), +): Symbol = + SwiftSymbol.make( + name, + declaration, + SwiftDependency.SMITHY_REST_XML, + additionalImports, + spiName, + ) From bfe3316821e071b1215a19bf212e19059031e0b6 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 30 Mar 2026 19:40:30 +0000 Subject: [PATCH 02/41] fix: Update MockHTTPRestXMLProtocolGenerator for schema-based - Add renderClientProtocol and plugins to mock customizations - Remove OperationInputBodyMiddleware and DeserializeMiddleware in mock - Fixes 4 pre-existing test failures --- .../MockHTTPRestXMLProtocolGenerator.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt index 2f34ec274..2ba70d701 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt @@ -10,17 +10,25 @@ 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.ShapeId +import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.DefaultHTTPProtocolCustomizations import software.amazon.smithy.swift.codegen.integration.HTTPBindingProtocolGenerator import software.amazon.smithy.swift.codegen.integration.HttpProtocolTestGenerator import software.amazon.smithy.swift.codegen.integration.HttpProtocolUnitTestErrorGenerator import software.amazon.smithy.swift.codegen.integration.HttpProtocolUnitTestRequestGenerator import software.amazon.smithy.swift.codegen.integration.HttpProtocolUnitTestResponseGenerator +import software.amazon.smithy.swift.codegen.integration.Plugin import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.isInHttpBody import software.amazon.smithy.swift.codegen.requestandresponse.TestHttpProtocolClientGeneratorFactory +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyRestXMLTypes -class MockRestXMLHTTPProtocolCustomizations : DefaultHTTPProtocolCustomizations() +class MockRestXMLHTTPProtocolCustomizations : DefaultHTTPProtocolCustomizations() { + override fun renderClientProtocol(writer: SwiftWriter): String = + writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) + + override val plugins: List = listOf() +} class MockHTTPRestXMLProtocolGenerator : HTTPBindingProtocolGenerator(MockRestXMLHTTPProtocolCustomizations()) { override val defaultContentType: String = "application/xml" @@ -32,7 +40,9 @@ class MockHTTPRestXMLProtocolGenerator : HTTPBindingProtocolGenerator(MockRestXM ctx: ProtocolGenerator.GenerationContext, operation: OperationShape, ) { - // Intentionally empty + // Remove middlewares handled by schema-based ClientProtocol + operationMiddleware.removeMiddleware(operation, "OperationInputBodyMiddleware") + operationMiddleware.removeMiddleware(operation, "DeserializeMiddleware") } override fun addUserAgentMiddleware( From 2a34000f263f7c2e95dc245b0e666f69c355de2e Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 30 Mar 2026 20:23:05 +0000 Subject: [PATCH 03/41] fix: Code review fixes - Add containerType: .structure to BaseError member schemas for correct memberName resolution - Fix xmlElementName fallback to check id.member before id.name - Remove unused XmlNameTrait import from HTTPClientProtocol --- Sources/SmithyRestXML/BaseError.swift | 3 +++ Sources/SmithyRestXML/Deserializer.swift | 2 +- Sources/SmithyRestXML/HTTPClientProtocol.swift | 1 - Sources/SmithyRestXML/Serializer.swift | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SmithyRestXML/BaseError.swift b/Sources/SmithyRestXML/BaseError.swift index 6f5d03a48..bee22bdfb 100644 --- a/Sources/SmithyRestXML/BaseError.swift +++ b/Sources/SmithyRestXML/BaseError.swift @@ -29,18 +29,21 @@ extension BaseError: DeserializableStruct { .init( id: .init("swift.synthetic", "RestXMLBaseError", "Code"), type: .member, + containerType: .structure, target: Prelude.stringSchema, index: 0 ), .init( id: .init("swift.synthetic", "RestXMLBaseError", "Message"), type: .member, + containerType: .structure, target: Prelude.stringSchema, index: 1 ), .init( id: .init("swift.synthetic", "RestXMLBaseError", "RequestId"), type: .member, + containerType: .structure, target: Prelude.stringSchema, index: 2 ), diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index f2ab15c5b..22ef0b921 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -229,7 +229,7 @@ public struct Deserializer: ShapeDeserializer { // MARK: - Private private func xmlElementName(for schema: Schema) -> String { - (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.name + (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name } } diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index d27a4fc8a..0fab896a5 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -15,7 +15,6 @@ import class Smithy.Context import struct Smithy.Schema import struct Smithy.ShapeID import struct Smithy.TargetsUnitTrait -import struct Smithy.XmlNameTrait import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index da875b7e4..f1309bd9c 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -161,7 +161,7 @@ public final class Serializer: ShapeSerializer { // MARK: - Private private func xmlElementName(for schema: Schema) -> String { - (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.name + (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name } private func xmlNamespaceAttr(for schema: Schema) -> String { From d4413eeee5c5ec3b40afcb96822af6deffaaa29d Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 30 Mar 2026 21:03:54 +0000 Subject: [PATCH 04/41] cleanup: Remove dead code in Serializer.writeScalar --- Sources/SmithyRestXML/Serializer.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index f1309bd9c..0c23f9c5d 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -173,16 +173,9 @@ public final class Serializer: ShapeSerializer { } private func writeScalar(schema: Schema, string: String) { - if schema.hasTrait(XmlAttributeTrait.self) { - // Attributes are handled during struct writing; this is a fallback - writeMemberOpen(schema: schema) - xmlParts.append(string) - writeMemberClose(schema: schema) - } else { - writeMemberOpen(schema: schema) - xmlParts.append(string) - writeMemberClose(schema: schema) - } + writeMemberOpen(schema: schema) + xmlParts.append(string) + writeMemberClose(schema: schema) } private func writeMemberOpen(schema: Schema) { From c521feb7ead43c462415c8a18a6185c1af06efbb Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 31 Mar 2026 18:09:47 +0000 Subject: [PATCH 05/41] cleanup: Remove unnecessary comments, match CBOR comment patterns --- Sources/SmithyRestXML/BaseError.swift | 1 - Sources/SmithyRestXML/Deserializer.swift | 6 ------ Sources/SmithyRestXML/HTTPClientProtocol.swift | 5 +---- Sources/SmithyRestXML/Plugin.swift | 2 -- Sources/SmithyRestXML/Serializer.swift | 4 ++-- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Sources/SmithyRestXML/BaseError.swift b/Sources/SmithyRestXML/BaseError.swift index bee22bdfb..9314c13a2 100644 --- a/Sources/SmithyRestXML/BaseError.swift +++ b/Sources/SmithyRestXML/BaseError.swift @@ -20,7 +20,6 @@ public struct BaseError { extension BaseError: DeserializableStruct { - /// Schema for the `` wrapper element in RestXML error responses. private static var errorSchema: Smithy.Schema { .init( id: .init("swift.synthetic", "RestXMLBaseError"), diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 22ef0b921..dced1aafd 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -29,7 +29,6 @@ public struct Deserializer: ShapeDeserializer { public init(data: Data) throws { if data.isEmpty { self.reader = try Reader.from(data: Data("".utf8)) - // Mark as empty by using a sentinel } else { self.reader = try Reader.from(data: data) } @@ -43,8 +42,6 @@ public struct Deserializer: ShapeDeserializer { schema.target ?? schema } - // MARK: - ShapeDeserializer - public func readStruct(_ schema: Schema, _ value: inout T) throws { let structSchema: Schema switch schema.type { @@ -179,7 +176,6 @@ public struct Deserializer: ShapeDeserializer { } public func readBigInteger(_ schema: Schema) throws -> Int64 { - // Reader doesn't have Int64 readIfPresent, read as String and convert guard let str: String = try reader.readIfPresent(), let value = Int64(str) else { throw XMLDeserializerError("Expected Int64 for \(schema.id)") } @@ -226,8 +222,6 @@ public struct Deserializer: ShapeDeserializer { public var containerSize: Int { reader.children.count } - // MARK: - Private - private func xmlElementName(for schema: Schema) -> String { (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name } diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 0fab896a5..0b37e11ed 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -69,12 +69,10 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { } else { let errorTypeRegistry = operation.errorTypeRegistry - // Parse the error response. RestXML errors are wrapped in ... - // unless noErrorWrapping is true. + // Parse error response; RestXML errors may be wrapped in element let errorDeserializer = try Deserializer(data: bodyData) let errorReader = errorDeserializer.reader - // Navigate to the element if wrapping is present let baseErrorDeserializer: Deserializer if noErrorWrapping { baseErrorDeserializer = errorDeserializer @@ -98,7 +96,6 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { } if let registryEntry { - // Re-deserialize the error body into the specific error type let specificDeserializer: Deserializer if noErrorWrapping { specificDeserializer = errorDeserializer diff --git a/Sources/SmithyRestXML/Plugin.swift b/Sources/SmithyRestXML/Plugin.swift index 6d644469c..f88872b0d 100644 --- a/Sources/SmithyRestXML/Plugin.swift +++ b/Sources/SmithyRestXML/Plugin.swift @@ -17,7 +17,5 @@ public struct Plugin: ClientRuntime.Plugin { public init() {} public func configureClient(clientConfiguration: inout Config) async throws { - // RestXML plugin currently has no additional configuration to apply. - // URL path and other HTTP bindings are handled by code-generated middlewares. } } diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 0c23f9c5d..298d55394 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -27,7 +27,7 @@ public final class Serializer: ShapeSerializer { public init() {} - // MARK: - ShapeSerializer + // MARK: - ShapeSerializer conformance public func writeStruct(_ schema: Schema, _ value: S) throws { let elementName = xmlElementName(for: schema) @@ -158,7 +158,7 @@ public final class Serializer: ShapeSerializer { Data(xmlParts.joined().utf8) } - // MARK: - Private + // MARK: - Private methods private func xmlElementName(for schema: Schema) -> String { (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name From d31e48239bd83215df64f5f6d8efc172e56e6cd2 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 31 Mar 2026 18:25:33 +0000 Subject: [PATCH 06/41] cleanup: Remove MARK comments to match codebase pattern --- Sources/SmithyRestXML/Serializer.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 298d55394..53c4d1e39 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -27,8 +27,6 @@ public final class Serializer: ShapeSerializer { public init() {} - // MARK: - ShapeSerializer conformance - public func writeStruct(_ schema: Schema, _ value: S) throws { let elementName = xmlElementName(for: schema) let nsAttr = xmlNamespaceAttr(for: schema) @@ -158,8 +156,6 @@ public final class Serializer: ShapeSerializer { Data(xmlParts.joined().utf8) } - // MARK: - Private methods - private func xmlElementName(for schema: Schema) -> String { (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name } From abae2abb34a4cb4524ea8f0e0ff9ea0f06dff36e Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 31 Mar 2026 18:39:14 +0000 Subject: [PATCH 07/41] refactor: Rewrite Serializer to wrap SmithyXML.Writer Instead of building XML via string concatenation, the Serializer now delegates to the existing SmithyXML.Writer which uses libxml2 for correct XML generation (encoding, entities, namespaces, etc). Three internal serializer types handle different contexts: - Serializer: top-level entry point, creates root Writer - MemberSerializer: writes struct members as child elements - ValueSerializer: writes values into list/map element nodes --- Sources/SmithyRestXML/Serializer.swift | 273 +++++++++++++------------ 1 file changed, 144 insertions(+), 129 deletions(-) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 53c4d1e39..0dbcd9888 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -19,180 +19,195 @@ import protocol SmithySerialization.SerializableStruct import struct SmithySerialization.SerializerError import protocol SmithySerialization.ShapeSerializer import typealias SmithySerialization.WriteValueConsumer +@_spi(SmithyReadWrite) import class SmithyXML.Writer +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo @_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat public final class Serializer: ShapeSerializer { - private var xmlParts: [String] = [] + private var rootWriter: Writer? public init() {} public func writeStruct(_ schema: Schema, _ value: S) throws { - let elementName = xmlElementName(for: schema) - let nsAttr = xmlNamespaceAttr(for: schema) - xmlParts.append("<\(elementName)\(nsAttr)>") + let nodeInfo = xmlNodeInfo(for: schema) + let writer: Writer + if let rootWriter { + writer = rootWriter + } else { + writer = Writer(nodeInfo: nodeInfo) + rootWriter = writer + } + let memberSerializer = MemberSerializer(parent: writer) for member in schema.members { - try S.writeConsumer(member, value, self) + try S.writeConsumer(member, value, memberSerializer) } - xmlParts.append("") } - public func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws { + public func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws {} + public func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws {} + public func writeBoolean(_ schema: Schema, _ value: Bool) throws {} + public func writeByte(_ schema: Schema, _ value: Int8) throws {} + public func writeShort(_ schema: Schema, _ value: Int16) throws {} + public func writeInteger(_ schema: Schema, _ value: Int) throws {} + public func writeLong(_ schema: Schema, _ value: Int) throws {} + public func writeFloat(_ schema: Schema, _ value: Float) throws {} + public func writeDouble(_ schema: Schema, _ value: Double) throws {} + public func writeBigInteger(_ schema: Schema, _ value: Int64) throws {} + public func writeBigDecimal(_ schema: Schema, _ value: Double) throws {} + public func writeString(_ schema: Schema, _ value: String) throws {} + public func writeBlob(_ schema: Schema, _ value: Data) throws {} + public func writeTimestamp(_ schema: Schema, _ value: Date) throws {} + public func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + throw SerializerError("Document type not supported in XML") + } + public func writeNull(_ schema: Schema) throws {} + + public var data: Data { + get throws { + guard let rootWriter else { return Data() } + return try rootWriter.data() + } + } +} + +/// Writes struct members as child elements of a parent Writer node. +/// This is the workhorse serializer — all member writes flow through here. +private final class MemberSerializer: ShapeSerializer { + let parent: Writer + + init(parent: Writer) { + self.parent = parent + } + + func writeStruct(_ schema: Schema, _ value: S) throws { + let child = parent[xmlNodeInfo(for: schema)] + let memberSerializer = MemberSerializer(parent: child) + for member in schema.members { + try S.writeConsumer(member, value, memberSerializer) + } + } + + func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws { let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) if isFlattened { - let elementName = xmlElementName(for: schema) + let nodeInfo = xmlNodeInfo(for: schema) for element in value { - xmlParts.append("<\(elementName)>") - try consumer(element, self) - xmlParts.append("") + try consumer(element, ValueSerializer(writer: parent[nodeInfo])) } } else { - writeMemberOpen(schema: schema) + let listWriter = parent[xmlNodeInfo(for: schema)] let memberSchema = (schema.target ?? schema).member - let memberName = xmlElementName(for: memberSchema) + let memberNodeInfo = xmlNodeInfo(for: memberSchema) for element in value { - xmlParts.append("<\(memberName)>") - try consumer(element, self) - xmlParts.append("") + try consumer(element, ValueSerializer(writer: listWriter[memberNodeInfo])) } - writeMemberClose(schema: schema) } } - public func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws { + func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws { let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) let mapSchema = schema.target ?? schema - let keyName = xmlElementName(for: mapSchema.key) - let valueName = xmlElementName(for: mapSchema.value) + let keyNodeInfo = xmlNodeInfo(for: mapSchema.key) + let valueNodeInfo = xmlNodeInfo(for: mapSchema.value) if isFlattened { - let elementName = xmlElementName(for: schema) + let nodeInfo = xmlNodeInfo(for: schema) for (k, v) in value { - xmlParts.append("<\(elementName)>") - xmlParts.append("<\(keyName)>\(xmlEscape(k))") - xmlParts.append("<\(valueName)>") - try consumer(v, self) - xmlParts.append("") - xmlParts.append("") + let entry = parent[nodeInfo] + try entry[keyNodeInfo].write(k) + try consumer(v, ValueSerializer(writer: entry[valueNodeInfo])) } } else { - writeMemberOpen(schema: schema) + let mapWriter = parent[xmlNodeInfo(for: schema)] for (k, v) in value { - xmlParts.append("") - xmlParts.append("<\(keyName)>\(xmlEscape(k))") - xmlParts.append("<\(valueName)>") - try consumer(v, self) - xmlParts.append("") - xmlParts.append("") + let entry = mapWriter[NodeInfo("entry")] + try entry[keyNodeInfo].write(k) + try consumer(v, ValueSerializer(writer: entry[valueNodeInfo])) } - writeMemberClose(schema: schema) } } - public func writeBoolean(_ schema: Schema, _ value: Bool) throws { - writeScalar(schema: schema, string: value ? "true" : "false") - } - - public func writeByte(_ schema: Schema, _ value: Int8) throws { - writeScalar(schema: schema, string: "\(value)") - } - - public func writeShort(_ schema: Schema, _ value: Int16) throws { - writeScalar(schema: schema, string: "\(value)") - } - - public func writeInteger(_ schema: Schema, _ value: Int) throws { - writeScalar(schema: schema, string: "\(value)") - } - - public func writeLong(_ schema: Schema, _ value: Int) throws { - writeScalar(schema: schema, string: "\(value)") - } - - public func writeFloat(_ schema: Schema, _ value: Float) throws { - if value.isNaN { writeScalar(schema: schema, string: "NaN") } - else if value == .infinity { writeScalar(schema: schema, string: "Infinity") } - else if value == -.infinity { writeScalar(schema: schema, string: "-Infinity") } - else { writeScalar(schema: schema, string: "\(value)") } - } - - public func writeDouble(_ schema: Schema, _ value: Double) throws { - if value.isNaN { writeScalar(schema: schema, string: "NaN") } - else if value == .infinity { writeScalar(schema: schema, string: "Infinity") } - else if value == -.infinity { writeScalar(schema: schema, string: "-Infinity") } - else { writeScalar(schema: schema, string: "\(value)") } - } - - public func writeBigInteger(_ schema: Schema, _ value: Int64) throws { - writeScalar(schema: schema, string: "\(value)") - } - - public func writeBigDecimal(_ schema: Schema, _ value: Double) throws { - try writeDouble(schema, value) - } - - public func writeString(_ schema: Schema, _ value: String) throws { - writeScalar(schema: schema, string: xmlEscape(value)) - } - - public func writeBlob(_ schema: Schema, _ value: Data) throws { - writeScalar(schema: schema, string: value.base64EncodedString()) - } - - public func writeTimestamp(_ schema: Schema, _ value: Date) throws { - let tsFormat = resolveTimestampFormat(schema) - let formatted = TimestampFormatter(format: tsFormat).string(from: value) - writeScalar(schema: schema, string: formatted) - } - - public func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + func writeBoolean(_ schema: Schema, _ value: Bool) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeByte(_ schema: Schema, _ value: Int8) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeShort(_ schema: Schema, _ value: Int16) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeInteger(_ schema: Schema, _ value: Int) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeLong(_ schema: Schema, _ value: Int) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeFloat(_ schema: Schema, _ value: Float) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeDouble(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBigInteger(_ schema: Schema, _ value: Int64) throws { try parent[xmlNodeInfo(for: schema)].write(String(value)) } + func writeBigDecimal(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeString(_ schema: Schema, _ value: String) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBlob(_ schema: Schema, _ value: Data) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeTimestamp(_ schema: Schema, _ value: Date) throws { + try parent[xmlNodeInfo(for: schema)].writeTimestamp(value, format: resolveTimestampFormat(schema)) + } + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { throw SerializerError("Document type not supported in XML") } + func writeNull(_ schema: Schema) throws {} + var data: Data { get throws { Data() } } +} - public func writeNull(_ schema: Schema) throws { - // Null not defined in XML. No operation. - } - - public var data: Data { - Data(xmlParts.joined().utf8) - } - - private func xmlElementName(for schema: Schema) -> String { - (try? schema.getTrait(XmlNameTrait.self))?.value ?? schema.memberName ?? schema.id.member ?? schema.id.name - } +/// Writes a value directly into a Writer node (used for list/map element consumers). +private struct ValueSerializer: ShapeSerializer { + let writer: Writer - private func xmlNamespaceAttr(for schema: Schema) -> String { - guard let ns = try? schema.getTrait(XmlNamespaceTrait.self) else { return "" } - if let prefix = ns.prefix, !prefix.isEmpty { - return " xmlns:\(prefix)=\"\(ns.uri)\"" + func writeStruct(_ schema: Schema, _ value: S) throws { + let memberSerializer = MemberSerializer(parent: writer) + for member in schema.members { + try S.writeConsumer(member, value, memberSerializer) } - return " xmlns=\"\(ns.uri)\"" } - private func writeScalar(schema: Schema, string: String) { - writeMemberOpen(schema: schema) - xmlParts.append(string) - writeMemberClose(schema: schema) + func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws { + let memberSchema = (schema.target ?? schema).member + let memberNodeInfo = xmlNodeInfo(for: memberSchema) + for element in value { + try consumer(element, ValueSerializer(writer: writer[memberNodeInfo])) + } } - private func writeMemberOpen(schema: Schema) { - guard let name = schema.memberName else { return } - let elementName = (try? schema.getTrait(XmlNameTrait.self))?.value ?? name - let nsAttr = xmlNamespaceAttr(for: schema) - xmlParts.append("<\(elementName)\(nsAttr)>") + func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws { + let mapSchema = schema.target ?? schema + let keyNodeInfo = xmlNodeInfo(for: mapSchema.key) + let valueNodeInfo = xmlNodeInfo(for: mapSchema.value) + for (k, v) in value { + let entry = writer[NodeInfo("entry")] + try entry[keyNodeInfo].write(k) + try consumer(v, ValueSerializer(writer: entry[valueNodeInfo])) + } } - private func writeMemberClose(schema: Schema) { - guard let name = schema.memberName else { return } - let elementName = (try? schema.getTrait(XmlNameTrait.self))?.value ?? name - xmlParts.append("") + func writeBoolean(_ schema: Schema, _ value: Bool) throws { try writer.write(value) } + func writeByte(_ schema: Schema, _ value: Int8) throws { try writer.write(value) } + func writeShort(_ schema: Schema, _ value: Int16) throws { try writer.write(value) } + func writeInteger(_ schema: Schema, _ value: Int) throws { try writer.write(value) } + func writeLong(_ schema: Schema, _ value: Int) throws { try writer.write(value) } + func writeFloat(_ schema: Schema, _ value: Float) throws { try writer.write(value) } + func writeDouble(_ schema: Schema, _ value: Double) throws { try writer.write(value) } + func writeBigInteger(_ schema: Schema, _ value: Int64) throws { try writer.write(String(value)) } + func writeBigDecimal(_ schema: Schema, _ value: Double) throws { try writer.write(value) } + func writeString(_ schema: Schema, _ value: String) throws { try writer.write(value) } + func writeBlob(_ schema: Schema, _ value: Data) throws { try writer.write(value) } + func writeTimestamp(_ schema: Schema, _ value: Date) throws { + try writer.writeTimestamp(value, format: resolveTimestampFormat(schema)) + } + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + throw SerializerError("Document type not supported in XML") } + func writeNull(_ schema: Schema) throws {} + var data: Data { get throws { Data() } } +} - private func xmlEscape(_ string: String) -> String { - string - .replacingOccurrences(of: "&", with: "&") - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "\"", with: """) - .replacingOccurrences(of: "'", with: "'") - } +private func xmlNodeInfo(for schema: Schema) -> NodeInfo { + let name = (try? schema.getTrait(XmlNameTrait.self))?.value + ?? schema.memberName ?? schema.id.member ?? schema.id.name + let location: NodeInfo.Location = schema.hasTrait(XmlAttributeTrait.self) ? .attribute : .element + let namespaceDef: NodeInfo.Namespace? + if let ns = try? schema.getTrait(XmlNamespaceTrait.self) { + namespaceDef = NodeInfo.Namespace(prefix: ns.prefix ?? "", uri: ns.uri) + } else { + namespaceDef = nil + } + return NodeInfo(name, location: location, namespaceDef: namespaceDef) } From 60bc387b570fcabf998caa0751e4ca377c9c8669 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 2 Apr 2026 18:28:18 +0000 Subject: [PATCH 08/41] fix: SwiftLint - sort imports and fix line length --- Sources/SmithyRestXML/Deserializer.swift | 6 +++--- Sources/SmithyRestXML/HTTPClientProtocol.swift | 4 ++-- Sources/SmithyRestXML/Serializer.swift | 14 ++++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index dced1aafd..4eb4d36fe 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -18,10 +18,10 @@ import typealias SmithySerialization.ReadStructConsumer import typealias SmithySerialization.ReadValueConsumer import struct SmithySerialization.SerializerError import protocol SmithySerialization.ShapeDeserializer -@_spi(SmithyReadWrite) import class SmithyXML.Reader -@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo -@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat +@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo +@_spi(SmithyReadWrite) import class SmithyXML.Reader public struct Deserializer: ShapeDeserializer { let reader: Reader diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 0b37e11ed..c06a9e7e4 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -18,13 +18,13 @@ import struct Smithy.TargetsUnitTrait import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse -@_spi(SmithyReadWrite) import class SmithyXML.Reader -@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo import protocol SmithySerialization.ClientProtocol import protocol SmithySerialization.Codec import protocol SmithySerialization.DeserializableStruct import struct SmithySerialization.Operation import struct SmithySerialization.TypeRegistry +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo +@_spi(SmithyReadWrite) import class SmithyXML.Reader public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { public typealias RequestType = HTTPRequest diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 0dbcd9888..767b80072 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -9,20 +9,20 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream import struct Smithy.Schema +import protocol Smithy.SmithyDocument import struct Smithy.TimestampFormatTrait import struct Smithy.XmlAttributeTrait import struct Smithy.XmlFlattenedTrait -import struct Smithy.XmlNameTrait import struct Smithy.XmlNamespaceTrait -import protocol Smithy.SmithyDocument +import struct Smithy.XmlNameTrait import protocol SmithySerialization.SerializableStruct import struct SmithySerialization.SerializerError import protocol SmithySerialization.ShapeSerializer import typealias SmithySerialization.WriteValueConsumer -@_spi(SmithyReadWrite) import class SmithyXML.Writer -@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo -@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat +@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter +@_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo +@_spi(SmithyReadWrite) import class SmithyXML.Writer public final class Serializer: ShapeSerializer { private var rootWriter: Writer? @@ -134,7 +134,9 @@ private final class MemberSerializer: ShapeSerializer { func writeLong(_ schema: Schema, _ value: Int) throws { try parent[xmlNodeInfo(for: schema)].write(value) } func writeFloat(_ schema: Schema, _ value: Float) throws { try parent[xmlNodeInfo(for: schema)].write(value) } func writeDouble(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeBigInteger(_ schema: Schema, _ value: Int64) throws { try parent[xmlNodeInfo(for: schema)].write(String(value)) } + func writeBigInteger(_ schema: Schema, _ value: Int64) throws { + try parent[xmlNodeInfo(for: schema)].write(String(value)) + } func writeBigDecimal(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } func writeString(_ schema: Schema, _ value: String) throws { try parent[xmlNodeInfo(for: schema)].write(value) } func writeBlob(_ schema: Schema, _ value: Data) throws { try parent[xmlNodeInfo(for: schema)].write(value) } From d7390b497924f0bcc15efdf001f4ec527b7e5dc6 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 6 Apr 2026 17:31:30 +0000 Subject: [PATCH 09/41] fix: Delete obsolete XML serde tests and update test models - Delete 18 XML serde test files in protocolspecificserde/xml/ that tested +Write.swift/+Read.swift codegen output no longer generated by schema-based codegen - Change pagination.smithy, pagination-truncation.smithy, waiters.smithy, and waiters-none.smithy from @restXml to @restJson1 since those tests don't test protocol-specific behavior --- .../xml/BlobDecodeXMLGenerationTests.kt | 52 --- .../xml/BlobEncodeXMLGenerationTests.kt | 44 -- .../xml/EnumDecodeXMLGenerationTests.kt | 55 --- .../xml/EnumEncodeXMLGenerationTests.kt | 47 --- .../xml/ListDecodeXMLGenerationTests.kt | 158 ------- .../xml/ListEncodeXMLGenerationTests.kt | 226 ---------- .../xml/MapDecodeXMLGenerationTests.kt | 398 ------------------ .../xml/MapEncodeXMLGenerationTests.kt | 317 -------------- .../xml/NamespaceEncodeXMLGenerationTests.kt | 119 ------ ...RecursiveShapesDecodeXMLGenerationTests.kt | 52 --- ...RecursiveShapesEncodeXMLGenerationTests.kt | 78 ---- .../xml/SetDecodeXMLGenerationTests.kt | 52 --- .../xml/SetEncodeXMLGenerationTests.kt | 44 -- .../xml/StructDecodeXMLGenerationTests.kt | 110 ----- .../xml/StructEncodeXMLGenerationTests.kt | 62 --- .../xml/TimeStampDecodeGenerationTests.kt | 98 ----- .../xml/TimeStampEncodeGenerationTests.kt | 96 ----- .../xml/UnionEncodeXMLGenerationTests.kt | 90 ---- .../resources/pagination-truncation.smithy | 4 +- .../src/test/resources/pagination.smithy | 4 +- .../src/test/resources/waiters-none.smithy | 4 +- .../src/test/resources/waiters.smithy | 4 +- 22 files changed, 8 insertions(+), 2106 deletions(-) delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/NamespaceEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructDecodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructEncodeXMLGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampDecodeGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampEncodeGenerationTests.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/UnionEncodeXMLGenerationTests.kt diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobDecodeXMLGenerationTests.kt deleted file mode 100644 index 1051c8737..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class BlobDecodeXMLGenerationTests { - @Test - fun `decode blob`() { - val context = setupTests("Isolated/Restxml/xml-blobs.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlBlobsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlBlobsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlBlobsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlBlobsOutput() - value.data = try reader["data"].readIfPresent() - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `decode blob nested`() { - val context = setupTests("Isolated/Restxml/xml-blobs.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlBlobsNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlBlobsNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlBlobsNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlBlobsNestedOutput() - value.nestedBlobList = try reader["nestedBlobList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readData(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobEncodeXMLGenerationTests.kt deleted file mode 100644 index f1ab57a54..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/BlobEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,44 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class BlobEncodeXMLGenerationTests { - @Test - fun `encode blob`() { - val context = setupTests("Isolated/Restxml/xml-blobs.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlBlobsInput+Write.swift") - val expectedContents = """ -extension XmlBlobsInput { - - static func write(value: XmlBlobsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["data"].write(value.data) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `encode nested blob`() { - val context = setupTests("Isolated/Restxml/xml-blobs.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlBlobsNestedInput+Write.swift") - val expectedContents = """ -extension XmlBlobsNestedInput { - - static func write(value: XmlBlobsNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedBlobList"].writeList(value.nestedBlobList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeData(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumDecodeXMLGenerationTests.kt deleted file mode 100644 index ed921c148..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class EnumDecodeXMLGenerationTests { - @Test - fun `decode enum`() { - val context = setupTests("Isolated/Restxml/xml-enums.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEnumsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEnumsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEnumsOutput() - value.fooEnum1 = try reader["fooEnum1"].readIfPresent() - value.fooEnum2 = try reader["fooEnum2"].readIfPresent() - value.fooEnum3 = try reader["fooEnum3"].readIfPresent() - value.fooEnumList = try reader["fooEnumList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosureBox().read(from:), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `decode enum nested`() { - val context = setupTests("Isolated/Restxml/xml-enums.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumsNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEnumsNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEnumsNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEnumsNestedOutput() - value.nestedEnumsList = try reader["nestedEnumsList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosureBox().read(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumEncodeXMLGenerationTests.kt deleted file mode 100644 index 30efa67f8..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/EnumEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,47 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class EnumEncodeXMLGenerationTests { - @Test - fun `001 encode enum`() { - val context = setupTests("Isolated/Restxml/xml-enums.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumsInput+Write.swift") - val expectedContents = """ -extension XmlEnumsInput { - - static func write(value: XmlEnumsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["fooEnum1"].write(value.fooEnum1) - try writer["fooEnum2"].write(value.fooEnum2) - try writer["fooEnum3"].write(value.fooEnum3) - try writer["fooEnumList"].writeList(value.fooEnumList, memberWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 encode nested enum`() { - val context = setupTests("Isolated/Restxml/xml-enums.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumsNestedInput+Write.swift") - val expectedContents = """ -extension XmlEnumsNestedInput { - - static func write(value: XmlEnumsNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedEnumsList"].writeList(value.nestedEnumsList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListDecodeXMLGenerationTests.kt deleted file mode 100644 index 05ff0862a..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,158 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class ListDecodeXMLGenerationTests { - @Test - fun `001 wrapped list with xmlName`() { - val context = setupTests("Isolated/Restxml/xml-lists-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListXmlNameOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlListXmlNameOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlListXmlNameOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlListXmlNameOutput() - value.renamedListMembers = try reader["renamed"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "item", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 wrapped nested list with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-lists-xmlname-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListXmlNameNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlListXmlNameNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlListXmlNameNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlListXmlNameNestedOutput() - value.renamedListMembers = try reader["renamed"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "subItem", isFlattened: false), memberNodeInfo: "item", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 decode flattened list`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened.smithy", "aws.protocoltests.restxml#RestXml") - - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlFlattenedListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlFlattenedListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlFlattenedListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlFlattenedListOutput() - value.myGroceryList = try reader["myGroceryList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 decode flattened empty list`() { - val context = setupTests("Isolated/Restxml/xml-lists-emptyFlattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEmptyFlattenedListsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEmptyFlattenedListsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEmptyFlattenedListsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEmptyFlattenedListsOutput() - value.booleanList = try reader["booleanList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readBool(from:), memberNodeInfo: "member", isFlattened: false) - value.integerList = try reader["integerList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), memberNodeInfo: "member", isFlattened: false) - value.stringList = try reader["stringList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: true) - value.stringSet = try reader["stringSet"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 decode nestednested flattened list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-nestednested-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedNestedFlattenedListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlNestedNestedFlattenedListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlNestedNestedFlattenedListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlNestedNestedFlattenedListOutput() - value.nestedNestedStringList = try reader["nestedNestedStringList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `012 decode list containing map`() { - val context = setupTests("Isolated/Restxml/xml-lists-contain-map.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListContainMapOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlListContainMapOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlListContainMapOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlListContainMapOutput() - value.myList = try reader["myList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `013 decode flattened list containing map`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened-contain-map.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlListFlattenedContainMapOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlListFlattenedContainMapOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlListFlattenedContainMapOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlListFlattenedContainMapOutput() - value.myList = try reader["myList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), memberNodeInfo: "member", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListEncodeXMLGenerationTests.kt deleted file mode 100644 index 89c930c6c..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/ListEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,226 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class ListEncodeXMLGenerationTests { - @Test - fun `001 wrapped list with xmlName`() { - val context = setupTests("Isolated/Restxml/xml-lists-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlListXmlNameInput { - - static func write(value: XmlListXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["renamed"].writeList(value.renamedListMembers, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "item", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 nested wrapped list with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-lists-xmlname-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListXmlNameNestedInput+Write.swift") - val expectedContents = """ -extension XmlListXmlNameNestedInput { - - static func write(value: XmlListXmlNameNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["renamed"].writeList(value.renamedListMembers, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "subItem", isFlattened: false), memberNodeInfo: "item", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 nested wrapped list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-nested-wrapped.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedWrappedListInput+Write.swift") - val expectedContents = """ -extension XmlNestedWrappedListInput { - - static func write(value: XmlNestedWrappedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedStringList"].writeList(value.nestedStringList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 nestednested wrapped list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-nestednested-wrapped.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedNestedWrappedListInput+Write.swift") - val expectedContents = """ -extension XmlNestedNestedWrappedListInput { - - static func write(value: XmlNestedNestedWrappedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedNestedStringList"].writeList(value.nestedNestedStringList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 nestednested flattened list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-nestednested-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedNestedFlattenedListInput+Write.swift") - val expectedContents = """ -extension XmlNestedNestedFlattenedListInput { - - static func write(value: XmlNestedNestedFlattenedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedNestedStringList"].writeList(value.nestedNestedStringList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `006 empty lists`() { - val context = setupTests("Isolated/Restxml/xml-lists-empty.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEmptyListsInput+Write.swift") - val expectedContents = """ -extension XmlEmptyListsInput { - - static func write(value: XmlEmptyListsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["booleanList"].writeList(value.booleanList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeBool(value:to:), memberNodeInfo: "member", isFlattened: false) - try writer["integerList"].writeList(value.integerList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeInt(value:to:), memberNodeInfo: "member", isFlattened: false) - try writer["stringList"].writeList(value.stringList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false) - try writer["stringSet"].writeList(value.stringSet, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `007 wrapped list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-wrapped.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlWrappedListInput+Write.swift") - val expectedContents = """ -extension XmlWrappedListInput { - - static func write(value: XmlWrappedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myGroceryList"].writeList(value.myGroceryList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `008 flattened list serialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlFlattenedListInput+Write.swift") - val expectedContents = """ -extension XmlFlattenedListInput { - - static func write(value: XmlFlattenedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myGroceryList"].writeList(value.myGroceryList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `010 encode nested flattened datetime encodable`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened-nested-datetime.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedFlattenedInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsNestedFlattenedInput { - - static func write(value: XmlTimestampsNestedFlattenedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedTimestampList"].writeList(value.nestedTimestampList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: .init("nestedMember", namespaceDef: .init(prefix: "baz", uri: "http://baz.com")), isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 encode flattened empty list`() { - val context = setupTests("Isolated/Restxml/xml-lists-emptyFlattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEmptyFlattenedListsInput+Write.swift") - val expectedContents = """ -extension XmlEmptyFlattenedListsInput { - - static func write(value: XmlEmptyFlattenedListsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["booleanList"].writeList(value.booleanList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeBool(value:to:), memberNodeInfo: "member", isFlattened: false) - try writer["integerList"].writeList(value.integerList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeInt(value:to:), memberNodeInfo: "member", isFlattened: false) - try writer["stringList"].writeList(value.stringList, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: true) - try writer["stringSet"].writeList(value.stringSet, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 encode list flattened nested with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened-nested-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListNestedFlattenedXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlListNestedFlattenedXmlNameInput { - - static func write(value: XmlListNestedFlattenedXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["listOfNestedStrings"].writeList(value.nestedList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "nestedNestedMember", isFlattened: false), memberNodeInfo: "nestedMember", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `012 encode list containing map`() { - val context = setupTests("Isolated/Restxml/xml-lists-contain-map.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListContainMapInput+Write.swift") - val expectedContents = """ -extension XmlListContainMapInput { - - static func write(value: XmlListContainMapInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myList"].writeList(value.myList, memberWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `013 encode flattened list containing map`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened-contain-map.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlListFlattenedContainMapInput+Write.swift") - val expectedContents = """ -extension XmlListFlattenedContainMapInput { - - static func write(value: XmlListFlattenedContainMapInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myList"].writeList(value.myList, memberWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), memberNodeInfo: "member", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapDecodeXMLGenerationTests.kt deleted file mode 100644 index 0fd4d37a4..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,398 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class MapDecodeXMLGenerationTests { - @Test - fun `001 decode wrapped map`() { - val context = setupTests("Isolated/Restxml/xml-maps.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 decode wrapped map with name protocol`() { - val context = setupTests("Isolated/Restxml/xml-maps.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsWithNameProtocolOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsWithNameProtocolOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsWithNameProtocolOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsWithNameProtocolOutput() - value.`protocol` = try reader["protocol"].readMapIfPresent(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 decode nested wrapped map`() { - val context = setupTests("Isolated/Restxml/xml-maps-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsNestedOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 decode nested nested wrapped map`() { - val context = setupTests("Isolated/Restxml/xml-maps-nestednested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsNestedNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsNestedNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsNestedNestedOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 decode flattened map`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlFlattenedMapsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlFlattenedMapsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlFlattenedMapsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlFlattenedMapsOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `006 decode flattened nested map`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedNestedOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `007 decode map with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsXmlNameOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsXmlNameOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsXmlNameOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "Attribute", valueNodeInfo: "Setting", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `008 decode map with xmlname flattened`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameFlattenedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsXmlNameFlattenedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsXmlNameFlattenedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsXmlNameFlattenedOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "SomeCustomKey", valueNodeInfo: "SomeCustomValue", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `009 decode map with xmlname nested`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsXmlNameNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsXmlNameNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsXmlNameNestedOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: RestXmlProtocolClientTypes.GreetingStruct.read(from:), keyNodeInfo: "CustomKey2", valueNodeInfo: "CustomValue2", isFlattened: false), keyNodeInfo: "CustomKey1", valueNodeInfo: "CustomValue1", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 decode flattened nested map with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedXmlNameOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedXmlNameOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedNestedXmlNameOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedNestedXmlNameOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "K", valueNodeInfo: "V", isFlattened: false), keyNodeInfo: "yek", valueNodeInfo: "eulav", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 decode map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNamespaceOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsXmlNamespaceOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsXmlNamespaceOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsXmlNamespaceOutput() - value.myMap = try reader[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: .init("Quality", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Degree", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `012 decode flattened map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedXmlNamespaceOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedXmlNamespaceOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedXmlNamespaceOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedXmlNamespaceOutput() - value.myMap = try reader[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: .init("Uid", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Val", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `013 decode nested map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-nested-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedXmlNamespaceOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsNestedXmlNamespaceOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsNestedXmlNamespaceOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsNestedXmlNamespaceOutput() - value.myMap = try reader[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: .init("K", namespaceDef: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespaceDef: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `014 decode nested flattened map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedXmlNamespaceOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedXmlNamespaceOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedNestedXmlNamespaceOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedNestedXmlNamespaceOutput() - value.myMap = try reader[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].readMapIfPresent(valueReadingClosure: SmithyReadWrite.mapReadingClosure(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: .init("K", namespaceDef: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespaceDef: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `015 decode map containing list`() { - val context = setupTests("Isolated/Restxml/xml-maps-contain-list.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsContainListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsContainListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsContainListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsContainListOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `016 decode flattened map containing list`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-contain-list.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedContainListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedContainListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedContainListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedContainListOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `017 decode map containing timestamp`() { - val context = setupTests("Isolated/Restxml/xml-maps-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsTimestampsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsTimestampsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsTimestampsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsTimestampsOutput() - value.timestampMap = try reader["timestampMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.timestampReadingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `018 decode flattened map containing timestamp`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedTimestampsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsFlattenedTimestampsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsFlattenedTimestampsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsFlattenedTimestampsOutput() - value.timestampMap = try reader["timestampMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.timestampReadingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `019 two maps that may conflict with KeyValue`() { - val context = setupTests("Isolated/Restxml/xml-maps-2x.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsTwoOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlMapsTwoOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlMapsTwoOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlMapsTwoOutput() - value.myMap = try reader["myMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - value.mySecondMap = try reader["mySecondMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapEncodeXMLGenerationTests.kt deleted file mode 100644 index 14ab914ef..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/MapEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,317 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class MapEncodeXMLGenerationTests { - @Test - fun `001 encode map`() { - val context = setupTests("Isolated/Restxml/xml-maps.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsInput+Write.swift") - val expectedContents = """ -extension XmlMapsInput { - - static func write(value: XmlMapsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 encode map with name protocol`() { - val context = setupTests("Isolated/Restxml/xml-maps.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsWithNameProtocolInput+Write.swift") - val expectedContents = """ -extension XmlMapsWithNameProtocolInput { - - static func write(value: XmlMapsWithNameProtocolInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["protocol"].writeMap(value.`protocol`, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 encode nested wrapped map`() { - val context = setupTests("Isolated/Restxml/xml-maps-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedInput+Write.swift") - val expectedContents = """ -extension XmlMapsNestedInput { - - static func write(value: XmlMapsNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 encode nested nested wrapped map`() { - val context = setupTests("Isolated/Restxml/xml-maps-nestednested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedNestedInput+Write.swift") - val expectedContents = """ -extension XmlMapsNestedNestedInput { - - static func write(value: XmlMapsNestedNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 encode flattened map`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlFlattenedMapsInput+Write.swift") - val expectedContents = """ -extension XmlFlattenedMapsInput { - - static func write(value: XmlFlattenedMapsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `006 encode flattened nested map`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedInput { - - static func write(value: XmlMapsFlattenedNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `007 encode map with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlMapsXmlNameInput { - - static func write(value: XmlMapsXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "Attribute", valueNodeInfo: "Setting", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `008 encode map with xmlname flattened`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname-flattened.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameFlattenedInput+Write.swift") - val expectedContents = """ -extension XmlMapsXmlNameFlattenedInput { - - static func write(value: XmlMapsXmlNameFlattenedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "SomeCustomKey", valueNodeInfo: "SomeCustomValue", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `009 encode map with xmlname nested`() { - val context = setupTests("Isolated/Restxml/xml-maps-xmlname-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNameNestedInput+Write.swift") - val expectedContents = """ -extension XmlMapsXmlNameNestedInput { - - static func write(value: XmlMapsXmlNameNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.write(value:to:), keyNodeInfo: "CustomKey2", valueNodeInfo: "CustomValue2", isFlattened: false), keyNodeInfo: "CustomKey1", valueNodeInfo: "CustomValue1", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `010 encode flattened nested map with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedXmlNameInput { - - static func write(value: XmlMapsFlattenedNestedXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: "K", valueNodeInfo: "V", isFlattened: false), keyNodeInfo: "yek", valueNodeInfo: "eulav", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 encode map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsXmlNamespaceInput+Write.swift") - val expectedContents = """ -extension XmlMapsXmlNamespaceInput { - - static func write(value: XmlMapsXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: .init("Quality", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Degree", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `012 encode flattened map xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedXmlNamespaceInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedXmlNamespaceInput { - - static func write(value: XmlMapsFlattenedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: .init("Uid", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Val", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `013 encode nested map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-nested-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsNestedXmlNamespaceInput+Write.swift") - val expectedContents = """ -extension XmlMapsNestedXmlNamespaceInput { - - static func write(value: XmlMapsNestedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: .init("K", namespaceDef: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespaceDef: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `014 encode nested flattened map with xmlnamespace`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-nested-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedNestedXmlNamespaceInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedNestedXmlNamespaceInput { - - static func write(value: XmlMapsFlattenedNestedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("myMap", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: .init("K", namespaceDef: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespaceDef: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespaceDef: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespaceDef: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `015 encode map containing list`() { - val context = setupTests("Isolated/Restxml/xml-maps-contain-list.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsContainListInput+Write.swift") - val expectedContents = """ -extension XmlMapsContainListInput { - - static func write(value: XmlMapsContainListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `016 encode flattened map containing list`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-contain-list.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedContainListInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedContainListInput { - - static func write(value: XmlMapsFlattenedContainListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["myMap"].writeMap(value.myMap, valueWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `017 encode map containing timestamp`() { - val context = setupTests("Isolated/Restxml/xml-maps-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsTimestampsInput+Write.swift") - val expectedContents = """ -extension XmlMapsTimestampsInput { - - static func write(value: XmlMapsTimestampsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["timestampMap"].writeMap(value.timestampMap, valueWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `017 encode flattened map containing timestamp`() { - val context = setupTests("Isolated/Restxml/xml-maps-flattened-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlMapsFlattenedTimestampsInput+Write.swift") - val expectedContents = """ -extension XmlMapsFlattenedTimestampsInput { - - static func write(value: XmlMapsFlattenedTimestampsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["timestampMap"].writeMap(value.timestampMap, valueWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `018 encode fooenumMap`() { - val context = setupTests("Isolated/Restxml/xml-maps-nested-fooenum.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/NestedXmlMapsInput+Write.swift") - val expectedContents = """ -extension NestedXmlMapsInput { - - static func write(value: NestedXmlMapsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["flatNestedMap"].writeMap(value.flatNestedMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: true) - try writer["nestedMap"].writeMap(value.nestedMap, valueWritingClosure: SmithyReadWrite.mapWritingClosure(valueWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/NamespaceEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/NamespaceEncodeXMLGenerationTests.kt deleted file mode 100644 index 5f2449737..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/NamespaceEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,119 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class NamespaceEncodeXMLGenerationTests { - @Test - fun `001 xmlnamespace, XmlNamespacesInput, Encodable`() { - val context = setupTests("Isolated/Restxml/xml-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespacesInput+Write.swift") - val expectedContents = """ -extension XmlNamespacesInput { - - static func write(value: XmlNamespacesInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("nested", namespaceDef: .init(prefix: "", uri: "http://boo.com"))].write(value.nested, with: RestXmlProtocolClientTypes.XmlNamespaceNested.write(value:to:)) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 xmlnamespace, XmlNamespaceNested`() { - val context = setupTests("Isolated/Restxml/xml-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespaceNested+ReadWrite.swift") - val expectedContents = """ -extension RestXmlProtocolClientTypes.XmlNamespaceNested { - - static func write(value: RestXmlProtocolClientTypes.XmlNamespaceNested?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("foo", namespaceDef: .init(prefix: "baz", uri: "http://baz.com"))].write(value.foo) - try writer[.init("values", namespaceDef: .init(prefix: "", uri: "http://qux.com"))].writeList(value.values, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: .init("member", namespaceDef: .init(prefix: "", uri: "http://bux.com")), isFlattened: false) - } - - static func read(from reader: SmithyXML.Reader) throws -> RestXmlProtocolClientTypes.XmlNamespaceNested { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = RestXmlProtocolClientTypes.XmlNamespaceNested() - value.foo = try reader[.init("foo", namespaceDef: .init(prefix: "baz", uri: "http://baz.com"))].readIfPresent() - value.values = try reader[.init("values", namespaceDef: .init(prefix: "", uri: "http://qux.com"))].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: .init("member", namespaceDef: .init(prefix: "", uri: "http://bux.com")), isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 xmlnamespace nested list, Encodable`() { - val context = setupTests("Isolated/Restxml/xml-namespace-nestedlist.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespaceNestedListInput+Write.swift") - val expectedContents = """ -extension XmlNamespaceNestedListInput { - - static func write(value: XmlNamespaceNestedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("nested", namespaceDef: .init(prefix: "", uri: "http://aux.com"))].writeList(value.nested, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: .init("member", namespaceDef: .init(prefix: "bzzzz", uri: "http://bar.com")), isFlattened: false), memberNodeInfo: .init("member", namespaceDef: .init(prefix: "baz", uri: "http://bux.com")), isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `007 xmlnamespace nested flattened list, encodable`() { - val context = setupTests("Isolated/Restxml/xml-namespace-flattenedlist.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespaceFlattenedListInput+Write.swift") - val expectedContents = """ -extension XmlNamespaceFlattenedListInput { - - static func write(value: XmlNamespaceFlattenedListInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer[.init("nested", namespaceDef: .init(prefix: "baz", uri: "http://aux.com"))].writeList(value.nested, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: true) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `010 xmlnamespace on service, encodable`() { - val context = setupTests("Isolated/Restxml/xml-namespace-onservice.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespacesOnServiceInput+Write.swift") - val expectedContents = """ -extension XmlNamespacesOnServiceInput { - - static func write(value: XmlNamespacesOnServiceInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["foo"].write(value.foo) - try writer[.init("nested", namespaceDef: .init(prefix: "xsi", uri: "https://example.com"))].write(value.nested, with: RestXmlProtocolClientTypes.NestedWithNamespace.write(value:to:)) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `011 xmlnamespace on service, encodable`() { - val context = setupTests("Isolated/Restxml/xml-namespace-onservice-overridable.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNamespacesOnServiceOverridableInput+Write.swift") - val expectedContents = """ -extension XmlNamespacesOnServiceOverridableInput { - - static func write(value: XmlNamespacesOnServiceOverridableInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["foo"].write(value.foo) - try writer[.init("nested", namespaceDef: .init(prefix: "xsi", uri: "https://example.com"))].write(value.nested, with: RestXmlProtocolClientTypes.NestedWithNamespace.write(value:to:)) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesDecodeXMLGenerationTests.kt deleted file mode 100644 index cf9026d9f..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class RecursiveShapesDecodeXMLGenerationTests { - @Test - fun `decode recursive shape`() { - val context = setupTests("Isolated/Restxml/xml-recursive.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlRecursiveShapesOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlRecursiveShapesOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlRecursiveShapesOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlRecursiveShapesOutput() - value.nested = try reader["nested"].readIfPresent(with: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.read(from:)) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `decode recursive nested shape`() { - val context = setupTests("Isolated/Restxml/xml-recursive-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedRecursiveShapesOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlNestedRecursiveShapesOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlNestedRecursiveShapesOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlNestedRecursiveShapesOutput() - value.nestedRecursiveList = try reader["nestedRecursiveList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.read(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesEncodeXMLGenerationTests.kt deleted file mode 100644 index c75daca59..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/RecursiveShapesEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,78 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class RecursiveShapesEncodeXMLGenerationTests { - @Test - fun `001 encode recursive shape Nested1`() { - val context = setupTests("Isolated/Restxml/xml-recursive.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/RecursiveShapesInputOutputNested1+ReadWrite.swift") - val expectedContents = """ -extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1 { - - static func write(value: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["foo"].write(value.foo) - try writer["nested"].write(value.nested, with: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2.write(value:to:)) - } - - static func read(from reader: SmithyXML.Reader) throws -> RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1 { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1() - value.foo = try reader["foo"].readIfPresent() - value.nested = try reader["nested"].readIfPresent(with: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2.read(from:)) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `encode recursive shape Nested2`() { - val context = setupTests("Isolated/Restxml/xml-recursive.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/RecursiveShapesInputOutputNested2+ReadWrite.swift") - val expectedContents = """ -extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2 { - - static func write(value: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["bar"].write(value.bar) - try writer["recursiveMember"].write(value.recursiveMember, with: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.write(value:to:)) - } - - static func read(from reader: SmithyXML.Reader) throws -> RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2 { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2() - value.bar = try reader["bar"].readIfPresent() - value.recursiveMember = try reader["recursiveMember"].readIfPresent(with: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.read(from:)) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `encode recursive nested shape`() { - val context = setupTests("Isolated/Restxml/xml-recursive-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedRecursiveShapesInput+Write.swift") - val expectedContents = """ -extension XmlNestedRecursiveShapesInput { - - static func write(value: XmlNestedRecursiveShapesInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedRecursiveList"].writeList(value.nestedRecursiveList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.write(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetDecodeXMLGenerationTests.kt deleted file mode 100644 index cbfc702dd..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class SetDecodeXMLGenerationTests { - @Test - fun `XmlEnumSetOutputBody decodable`() { - val context = setupTests("Isolated/Restxml/xml-sets.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumSetOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEnumSetOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEnumSetOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEnumSetOutput() - value.fooEnumSet = try reader["fooEnumSet"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosureBox().read(from:), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `XmlEnumNestedSetOutputBody nested decodable`() { - val context = setupTests("Isolated/Restxml/xml-sets-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumNestedSetOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEnumNestedSetOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEnumNestedSetOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEnumNestedSetOutput() - value.fooEnumSet = try reader["fooEnumSet"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosureBox().read(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetEncodeXMLGenerationTests.kt deleted file mode 100644 index fb07ddfd1..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/SetEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,44 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class SetEncodeXMLGenerationTests { - @Test - fun `001 wrapped set serialization`() { - val context = setupTests("Isolated/Restxml/xml-sets.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumSetInput+Write.swift") - val expectedContents = """ -extension XmlEnumSetInput { - - static func write(value: XmlEnumSetInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["fooEnumSet"].writeList(value.fooEnumSet, memberWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 wrapped nested set serialization`() { - val context = setupTests("Isolated/Restxml/xml-sets-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEnumNestedSetInput+Write.swift") - val expectedContents = """ -extension XmlEnumNestedSetInput { - - static func write(value: XmlEnumNestedSetInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["fooEnumSet"].writeList(value.fooEnumSet, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosureBox().write(value:to:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructDecodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructDecodeXMLGenerationTests.kt deleted file mode 100644 index ed3d7d42b..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructDecodeXMLGenerationTests.kt +++ /dev/null @@ -1,110 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class StructDecodeXMLGenerationTests { - @Test - fun `XmlWrappedListOutputBody decodable`() { - val context = setupTests("Isolated/Restxml/xml-lists-wrapped.smithy", "aws.protocoltests.restxml#RestXml") - - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlWrappedListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlWrappedListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlWrappedListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlWrappedListOutput() - value.myGroceryList = try reader["myGroceryList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `SimpleScalarPropertiesOutputBody decodable`() { - val context = setupTests("Isolated/Restxml/xml-scalar.smithy", "aws.protocoltests.restxml#RestXml") - - val contents = getFileContents(context.manifest, "Sources/RestXml/models/SimpleScalarPropertiesOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension SimpleScalarPropertiesOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> SimpleScalarPropertiesOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = SimpleScalarPropertiesOutput() - if let fooHeaderValue = httpResponse.headers.value(for: "X-Foo") { - value.foo = fooHeaderValue - } - value.byteValue = try reader["byteValue"].readIfPresent() - value.doubleValue = try reader["DoubleDribble"].readIfPresent() - value.falseBooleanValue = try reader["falseBooleanValue"].readIfPresent() - value.floatValue = try reader["floatValue"].readIfPresent() - value.integerValue = try reader["integerValue"].readIfPresent() - value.longValue = try reader["longValue"].readIfPresent() - value.`protocol` = try reader["protocol"].readIfPresent() - value.shortValue = try reader["shortValue"].readIfPresent() - value.stringValue = try reader["stringValue"].readIfPresent() - value.trueBooleanValue = try reader["trueBooleanValue"].readIfPresent() - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `nestednested wrapped list deserialization`() { - val context = setupTests("Isolated/Restxml/xml-lists-nestednested-wrapped.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlNestedNestedWrappedListOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlNestedNestedWrappedListOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlNestedNestedWrappedListOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlNestedNestedWrappedListOutput() - value.nestedNestedStringList = try reader["nestedNestedStringList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `empty lists decode`() { - val context = setupTests("Isolated/Restxml/xml-lists-empty.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlEmptyListsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlEmptyListsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlEmptyListsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlEmptyListsOutput() - value.booleanList = try reader["booleanList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readBool(from:), memberNodeInfo: "member", isFlattened: false) - value.integerList = try reader["integerList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), memberNodeInfo: "member", isFlattened: false) - value.stringList = try reader["stringList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false) - value.stringSet = try reader["stringSet"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructEncodeXMLGenerationTests.kt deleted file mode 100644 index 940d00b31..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/StructEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class StructEncodeXMLGenerationTests { - @Test - fun `simpleScalar serialization`() { - val context = setupTests("Isolated/Restxml/xml-scalar.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/SimpleScalarPropertiesInput+Write.swift") - val expectedContents = """ -extension SimpleScalarPropertiesInput { - - static func write(value: SimpleScalarPropertiesInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["byteValue"].write(value.byteValue) - try writer["DoubleDribble"].write(value.doubleValue) - try writer["falseBooleanValue"].write(value.falseBooleanValue) - try writer["floatValue"].write(value.floatValue) - try writer["integerValue"].write(value.integerValue) - try writer["longValue"].write(value.longValue) - try writer["protocol"].write(value.`protocol`) - try writer["shortValue"].write(value.shortValue) - try writer["stringValue"].write(value.stringValue) - try writer["trueBooleanValue"].write(value.trueBooleanValue) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `008 structure xmlName`() { - val context = setupTests("Isolated/Restxml/xml-lists-structure.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/StructureListMember+ReadWrite.swift") - val expectedContents = """ -extension RestXmlProtocolClientTypes.StructureListMember { - - static func write(value: RestXmlProtocolClientTypes.StructureListMember?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["value"].write(value.a) - try writer["other"].write(value.b) - } - - static func read(from reader: SmithyXML.Reader) throws -> RestXmlProtocolClientTypes.StructureListMember { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = RestXmlProtocolClientTypes.StructureListMember() - value.a = try reader["value"].readIfPresent() - value.b = try reader["other"].readIfPresent() - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampDecodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampDecodeGenerationTests.kt deleted file mode 100644 index 3159402b0..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampDecodeGenerationTests.kt +++ /dev/null @@ -1,98 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class TimeStampDecodeGenerationTests { - @Test - fun `001 decode all timestamps`() { - val context = setupTests("Isolated/Restxml/xml-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlTimestampsOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlTimestampsOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlTimestampsOutput() - value.dateTime = try reader["dateTime"].readTimestampIfPresent(format: SmithyTimestamps.TimestampFormat.dateTime) - value.epochSeconds = try reader["epochSeconds"].readTimestampIfPresent(format: SmithyTimestamps.TimestampFormat.epochSeconds) - value.httpDate = try reader["httpDate"].readTimestampIfPresent(format: SmithyTimestamps.TimestampFormat.httpDate) - value.normal = try reader["normal"].readTimestampIfPresent(format: SmithyTimestamps.TimestampFormat.dateTime) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 decode nested timestamps`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlTimestampsNestedOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlTimestampsNestedOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlTimestampsNestedOutput() - value.nestedTimestampList = try reader["nestedTimestampList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.timestampReadingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 decode nested timestamps HttpDate`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedHTTPDateOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlTimestampsNestedHTTPDateOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlTimestampsNestedHTTPDateOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlTimestampsNestedHTTPDateOutput() - value.nestedTimestampList = try reader["nestedTimestampList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.timestampReadingClosure(format: SmithyTimestamps.TimestampFormat.httpDate), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - return value - } -} -""" - - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 decode nested timestamps xmlName`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = - getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedXmlNameOutput+HttpResponseBinding.swift") - val expectedContents = """ -extension XmlTimestampsNestedXmlNameOutput { - - static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> XmlTimestampsNestedXmlNameOutput { - let data = try await httpResponse.data() - let responseReader = try SmithyXML.Reader.from(data: data) - let reader = responseReader - var value = XmlTimestampsNestedXmlNameOutput() - value.nestedTimestampList = try reader["nestedTimestampList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.timestampReadingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), memberNodeInfo: "nestedTag2", isFlattened: false), memberNodeInfo: "nestedTag1", isFlattened: false) - return value - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampEncodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampEncodeGenerationTests.kt deleted file mode 100644 index 56814b9b1..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/TimeStampEncodeGenerationTests.kt +++ /dev/null @@ -1,96 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class TimeStampEncodeGenerationTests { - @Test - fun `001 encode all timestamps`() { - val context = setupTests("Isolated/Restxml/xml-timestamp.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsInput { - - static func write(value: XmlTimestampsInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["dateTime"].writeTimestamp(value.dateTime, format: SmithyTimestamps.TimestampFormat.dateTime) - try writer["epochSeconds"].writeTimestamp(value.epochSeconds, format: SmithyTimestamps.TimestampFormat.epochSeconds) - try writer["httpDate"].writeTimestamp(value.httpDate, format: SmithyTimestamps.TimestampFormat.httpDate) - try writer["normal"].writeTimestamp(value.normal, format: SmithyTimestamps.TimestampFormat.dateTime) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 encode nested list with timestamps`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsNestedInput { - - static func write(value: XmlTimestampsNestedInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedTimestampList"].writeList(value.nestedTimestampList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `003 encode nested list with timestamps httpDate`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedHTTPDateInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsNestedHTTPDateInput { - - static func write(value: XmlTimestampsNestedHTTPDateInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedTimestampList"].writeList(value.nestedTimestampList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.httpDate), memberNodeInfo: "member", isFlattened: false), memberNodeInfo: "member", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `004 encode nested list with timestamps with xmlname`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-nested-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsNestedXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsNestedXmlNameInput { - - static func write(value: XmlTimestampsNestedXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["nestedTimestampList"].writeList(value.nestedTimestampList, memberWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.timestampWritingClosure(format: SmithyTimestamps.TimestampFormat.epochSeconds), memberNodeInfo: "nestedTag2", isFlattened: false), memberNodeInfo: "nestedTag1", isFlattened: false) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `005 encode all timestamps, withxmlName`() { - val context = setupTests("Isolated/Restxml/xml-timestamp-xmlname.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlTimestampsXmlNameInput+Write.swift") - val expectedContents = """ -extension XmlTimestampsXmlNameInput { - - static func write(value: XmlTimestampsXmlNameInput?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - try writer["dateTime"].writeTimestamp(value.dateTime, format: SmithyTimestamps.TimestampFormat.dateTime) - try writer["notNormalName"].writeTimestamp(value.normal, format: SmithyTimestamps.TimestampFormat.dateTime) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/UnionEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/UnionEncodeXMLGenerationTests.kt deleted file mode 100644 index 8f6b551a4..000000000 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/xml/UnionEncodeXMLGenerationTests.kt +++ /dev/null @@ -1,90 +0,0 @@ -package software.amazon.smithy.swift.codegen.protocolspecificserde.xml - -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.getFileContents - -class UnionEncodeXMLGenerationTests { - @Test - fun `001 XmlUnionShape+Codable`() { - val context = setupTests("Isolated/Restxml/xml-unions.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlUnionShape+ReadWrite.swift") - val expectedContents = """ -extension RestXmlProtocolClientTypes.XmlUnionShape { - - static func write(value: RestXmlProtocolClientTypes.XmlUnionShape?, to writer: SmithyXML.Writer) throws { - guard let value else { return } - switch value { - case let .datavalue(datavalue): - try writer["dataValue"].write(datavalue) - case let .doublevalue(doublevalue): - try writer["doubleValue"].write(doublevalue) - case let .mapvalue(mapvalue): - try writer["mapValue"].writeMap(mapvalue, valueWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), keyNodeInfo: "K", valueNodeInfo: "V", isFlattened: false) - case let .stringlist(stringlist): - try writer["stringList"].writeList(stringlist, memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false) - case let .structvalue(structvalue): - try writer["structValue"].write(structvalue, with: RestXmlProtocolClientTypes.XmlNestedUnionStruct.write(value:to:)) - case let .timestampvalue(timestampvalue): - try writer["timeStampValue"].writeTimestamp(timestampvalue, format: SmithyTimestamps.TimestampFormat.dateTime) - case let .unionvalue(unionvalue): - try writer["unionValue"].write(unionvalue, with: RestXmlProtocolClientTypes.XmlUnionShape.write(value:to:)) - case let .sdkUnknown(sdkUnknown): - try writer["sdkUnknown"].write(sdkUnknown) - } - } - - static func read(from reader: SmithyXML.Reader) throws -> RestXmlProtocolClientTypes.XmlUnionShape { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - let name = reader.children.filter { ${'$'}0.hasContent && ${'$'}0.nodeInfo.name != "__type" }.first?.nodeInfo.name - switch name { - case "doubleValue": - return .doublevalue(try reader["doubleValue"].read()) - case "dataValue": - return .datavalue(try reader["dataValue"].read()) - case "unionValue": - return .unionvalue(try reader["unionValue"].read(with: RestXmlProtocolClientTypes.XmlUnionShape.read(from:))) - case "structValue": - return .structvalue(try reader["structValue"].read(with: RestXmlProtocolClientTypes.XmlNestedUnionStruct.read(from:))) - case "mapValue": - return .mapvalue(try reader["mapValue"].readMap(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "K", valueNodeInfo: "V", isFlattened: false)) - case "stringList": - return .stringlist(try reader["stringList"].readList(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false)) - case "timeStampValue": - return .timestampvalue(try reader["timeStampValue"].readTimestamp(format: SmithyTimestamps.TimestampFormat.dateTime)) - default: - return .sdkUnknown(name ?? "") - } - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `002 XmlUnionShape should be marked as indirect`() { - val context = setupTests("Isolated/Restxml/xml-unions.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "Sources/RestXml/models/XmlUnionShape.swift") - val expectedContents = """ -extension ExampleClientTypes { - - public indirect enum XmlUnionShape: Swift.Sendable { - case doublevalue(Swift.Double) - case datavalue(Foundation.Data) - case unionvalue(ExampleClientTypes.XmlUnionShape) - case structvalue(ExampleClientTypes.XmlNestedUnionStruct) - case mapvalue([Swift.String: Swift.String]) - case stringlist([Swift.String]) - case timestampvalue(Foundation.Date) - case sdkUnknown(Swift.String) - } -} -""" - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy b/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy index 6ab35cada..74fdd2b84 100644 --- a/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy +++ b/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy @@ -2,12 +2,12 @@ $version: "1.0" namespace software.amazon.smithy.swift.codegen.synthetic -use aws.protocols#restXml +use aws.protocols#restJson1 @trait(selector: "*") structure paginationTruncationMember { } -@restXml +@restJson1 service Lambda { operations: [ListFunctionsTruncated] } diff --git a/smithy-swift-codegen/src/test/resources/pagination.smithy b/smithy-swift-codegen/src/test/resources/pagination.smithy index f0296e646..978a6153e 100644 --- a/smithy-swift-codegen/src/test/resources/pagination.smithy +++ b/smithy-swift-codegen/src/test/resources/pagination.smithy @@ -1,8 +1,8 @@ namespace com.test -use aws.protocols#restXml +use aws.protocols#restJson1 -@restXml +@restJson1 service Lambda { operations: [ListFunctions, ListFunctions2, ListFunctions3] } diff --git a/smithy-swift-codegen/src/test/resources/waiters-none.smithy b/smithy-swift-codegen/src/test/resources/waiters-none.smithy index e9fb58848..927f009b8 100644 --- a/smithy-swift-codegen/src/test/resources/waiters-none.smithy +++ b/smithy-swift-codegen/src/test/resources/waiters-none.smithy @@ -1,9 +1,9 @@ namespace com.test use smithy.waiters#waitable -use aws.protocols#restXml +use aws.protocols#restJson1 -@restXml +@restJson1 service TestHasNoWaiters { operations: [NoWaiting] } diff --git a/smithy-swift-codegen/src/test/resources/waiters.smithy b/smithy-swift-codegen/src/test/resources/waiters.smithy index 2960c90dd..c865e4f49 100644 --- a/smithy-swift-codegen/src/test/resources/waiters.smithy +++ b/smithy-swift-codegen/src/test/resources/waiters.smithy @@ -1,9 +1,9 @@ namespace com.test use smithy.waiters#waitable -use aws.protocols#restXml +use aws.protocols#restJson1 -@restXml +@restJson1 service TestHasWaiters { operations: [HeadBucket] } From 6c3e7a3de85b9b51cd6d939c554371b882c8ba21 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 17 Apr 2026 14:55:16 -0400 Subject: [PATCH 10/41] fix: Resolve ktlint function-signature violations Inline single-expression function bodies onto signature line to satisfy standard:function-signature rule. - RestXmlCustomizations.kt:18 (renderClientProtocol) - MockHTTPRestXMLProtocolGenerator.kt:27 (renderClientProtocol) - SerdeUtils.kt:11 (useSchemaBased delegating overload) --- .../codegen/aws/protocols/restxml/RestXmlCustomizations.kt | 3 +-- .../smithy/swift/codegen/integration/serde/SerdeUtils.kt | 3 +-- .../protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt index ff7cdb769..dc73cf8a6 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/aws/protocols/restxml/RestXmlCustomizations.kt @@ -15,8 +15,7 @@ import software.amazon.smithy.swift.codegen.swiftmodules.SmithyRestXMLTypes open class RestXmlCustomizations : DefaultHTTPProtocolCustomizations() { override val baseErrorSymbol: Symbol = ClientRuntimeTypes.RestXML.RestXMLError - override fun renderClientProtocol(writer: SwiftWriter): String = - writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) + override fun renderClientProtocol(writer: SwiftWriter): String = writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) override val plugins: List = listOf(RestXMLPlugin()) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt index 8e92c2850..e10690304 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt @@ -8,8 +8,7 @@ import software.amazon.smithy.swift.codegen.model.hasTrait class SerdeUtils { companion object { - fun useSchemaBased(ctx: ProtocolGenerator.GenerationContext) = - useSchemaBased(ctx.service) + fun useSchemaBased(ctx: ProtocolGenerator.GenerationContext) = useSchemaBased(ctx.service) fun useSchemaBased(service: ServiceShape) = // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt index 2ba70d701..ec3b7ce31 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolgeneratormocks/MockHTTPRestXMLProtocolGenerator.kt @@ -24,8 +24,7 @@ import software.amazon.smithy.swift.codegen.requestandresponse.TestHttpProtocolC import software.amazon.smithy.swift.codegen.swiftmodules.SmithyRestXMLTypes class MockRestXMLHTTPProtocolCustomizations : DefaultHTTPProtocolCustomizations() { - override fun renderClientProtocol(writer: SwiftWriter): String = - writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) + override fun renderClientProtocol(writer: SwiftWriter): String = writer.format("\$N()", SmithyRestXMLTypes.HTTPClientProtocol) override val plugins: List = listOf() } From 0e562caa42a53729e483343dc76de38a311a4178 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Sat, 18 Apr 2026 10:50:27 -0400 Subject: [PATCH 11/41] fix: Generate HTTP binding providers for schema-based RestXML RestXML is a REST-style protocol that uses HTTP URI/header/query bindings, unlike AwsJson and RpcV2Cbor. The generated client's OperationInputUrlPathMiddleware, OperationInputHeadersMiddleware, and OperationInputQueryItemMiddleware reference Input.urlPathProvider(_:), headerProvider(_:), and queryItemProvider(_:), so those static funcs must still be emitted on Input extensions even when serde is schema- based. Extend the guard to keep emitting these providers for RestXML. --- .../swift/codegen/integration/HTTPBindingProtocolGenerator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt index 83bc9383c..0e5e9b07c 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.swift.codegen.integration import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait +import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex @@ -149,7 +150,7 @@ abstract class HTTPBindingProtocolGenerator( continue } val httpBindingResolver = getProtocolHttpBindingResolver(ctx, defaultContentType) - if (!usesSchemaBased) { + if (!usesSchemaBased || ctx.service.hasTrait()) { HttpUrlPathProvider.renderUrlPathMiddleware(ctx, operation, httpBindingResolver) HttpHeaderProvider.renderHeaderMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) HttpQueryItemProvider.renderQueryMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) From 20e21bfe453e20f845e7a947c4e9bff84f001b14 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 20 Apr 2026 11:58:08 -0400 Subject: [PATCH 12/41] chore: Retrigger CI to pick up companion fix (SmithyRestXML dep in protocol tests) From 7bdc134bcef49c2946c32c6f99ef9fe497f830dd Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 20 Apr 2026 13:14:35 -0400 Subject: [PATCH 13/41] fix(SmithyRestXML): Avoid libxml2 parse on empty response body The Deserializer was using Data("".utf8) as a substitute for empty response bodies, then parsing it via libxml2. For tiny payloads Swift may use inline Data storage, creating a stack-pointer lifetime issue with xmlBufferCreateStatic that causes xmlReadMemory to fail with 'The XML could not be parsed'. Return an empty Reader directly for empty data instead. This resolves ~16 RestXML protocol tests where responses have no body (headers-only outputs, EmptyInputAndEmptyOutput, NoInputAndNoOutput, etc.). Expose Reader's default init as public under the existing SmithyReadWrite SPI so Deserializer (in SmithyRestXML) can construct an empty Reader. --- Sources/SmithyRestXML/Deserializer.swift | 2 +- Sources/SmithyXML/Reader/Reader.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 4eb4d36fe..39ed5dbeb 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -28,7 +28,7 @@ public struct Deserializer: ShapeDeserializer { public init(data: Data) throws { if data.isEmpty { - self.reader = try Reader.from(data: Data("".utf8)) + self.reader = Reader() } else { self.reader = try Reader.from(data: data) } diff --git a/Sources/SmithyXML/Reader/Reader.swift b/Sources/SmithyXML/Reader/Reader.swift index 88189cf8a..04e3639d3 100644 --- a/Sources/SmithyXML/Reader/Reader.swift +++ b/Sources/SmithyXML/Reader/Reader.swift @@ -26,7 +26,7 @@ public final class Reader: SmithyReader { // MARK: - init & deinit /// Creates an "empty" reader. This Reader will be returned when the data cannot be parsed. - init() { + public init() { self.nodeInfo = "" } From 5ecbe05fa484dc184d15ae115a22b1eb9a55daec Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 20 Apr 2026 16:34:02 -0400 Subject: [PATCH 14/41] feat(SmithyRestXML): Extract HTTP response bindings in schema-based deserialization Schema-based RestXML is the first schema-based protocol that needs HTTP response bindings (headers, prefix headers, response code, httpPayload). Earlier schema-based protocols (AwsJson, CBOR) are RPC-style and never need them. The deserialize path previously passed only the body bytes through the XML Reader, silently leaving header-/status-/payload-bound output members unset (or throwing when a non-XML raw payload hit the parser). Add the HTTP binding trait types that the Swift schema needs: - HttpHeaderTrait (string value: header name) - HttpPrefixHeadersTrait (string value: header name prefix) - HttpResponseCodeTrait (marker) - HttpPayloadTrait (marker) Register them in AllSupportedTraits so Swift SchemasCodegen emits them into generated schemas. Extend Deserializer to optionally carry the HTTPResponse + raw body data. In readStruct, route each member through httpBindingDeserializer which inspects the member's schema traits and, if HTTP-bound, returns a synthetic child Deserializer sourced from the header value, status code, or raw body bytes (as applicable) instead of the XML reader. Normal body-bound members keep the existing XML element lookup path untouched. Expose Reader.init(content:) and Reader.addChild publicly under the existing SmithyReadWrite SPI so the Deserializer can synthesize a Reader wrapping a single header value or a list of split values. HTTPClientProtocol.deserializeResponse now constructs a binding-aware Deserializer directly (passing through the HTTPResponse + bodyData) instead of going through codec.makeDeserializer which only accepts body data. The error path is unchanged. --- .../TraitLibrary/AllSupportedTraits.swift | 4 + .../Smithy/TraitLibrary/HttpHeaderTrait.swift | 19 +++ .../TraitLibrary/HttpPayloadTrait.swift | 12 ++ .../TraitLibrary/HttpPrefixHeadersTrait.swift | 19 +++ .../TraitLibrary/HttpResponseCodeTrait.swift | 12 ++ Sources/SmithyRestXML/Deserializer.swift | 119 +++++++++++++++++- .../SmithyRestXML/HTTPClientProtocol.swift | 2 +- Sources/SmithyXML/Reader/Reader.swift | 9 +- 8 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift index e16ec832b..9d4c591c6 100644 --- a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -26,6 +26,10 @@ private let allSupportedTraitTypes: [ShapeID: any Trait.Type] = [ ErrorTrait.id: ErrorTrait.self, EventHeaderTrait.id: EventHeaderTrait.self, EventPayloadTrait.id: EventPayloadTrait.self, + HttpHeaderTrait.id: HttpHeaderTrait.self, + HttpPayloadTrait.id: HttpPayloadTrait.self, + HttpPrefixHeadersTrait.id: HttpPrefixHeadersTrait.self, + HttpResponseCodeTrait.id: HttpResponseCodeTrait.self, InputTrait.id: InputTrait.self, OutputTrait.id: OutputTrait.self, RequiredTrait.id: RequiredTrait.self, diff --git a/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift b/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift new file mode 100644 index 000000000..d14162ed5 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpHeaderTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpHeader") } + public let value: String + public var node: Node { .string(value) } + + public init(node: Node) throws { + guard case .string(let value) = node else { + throw TraitError("httpHeader trait requires a string value") + } + self.value = value + } +} diff --git a/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift b/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift new file mode 100644 index 000000000..23e32bc9f --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpPayloadTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpPayload") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift b/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift new file mode 100644 index 000000000..304bbdf9c --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpPrefixHeadersTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpPrefixHeaders") } + public let value: String + public var node: Node { .string(value) } + + public init(node: Node) throws { + guard case .string(let value) = node else { + throw TraitError("httpPrefixHeaders trait requires a string value") + } + self.value = value + } +} diff --git a/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift b/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift new file mode 100644 index 000000000..a25aa80ae --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpResponseCodeTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpResponseCode") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 39ed5dbeb..d4fbfeb19 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -8,11 +8,16 @@ import struct Foundation.Data import struct Foundation.Date import struct Smithy.Document +import struct Smithy.HttpHeaderTrait +import struct Smithy.HttpPayloadTrait +import struct Smithy.HttpPrefixHeadersTrait +import struct Smithy.HttpResponseCodeTrait import struct Smithy.Schema import struct Smithy.TimestampFormatTrait import struct Smithy.XmlAttributeTrait import struct Smithy.XmlFlattenedTrait import struct Smithy.XmlNameTrait +import class SmithyHTTPAPI.HTTPResponse import protocol SmithySerialization.DeserializableStruct import typealias SmithySerialization.ReadStructConsumer import typealias SmithySerialization.ReadValueConsumer @@ -25,8 +30,12 @@ import protocol SmithySerialization.ShapeDeserializer public struct Deserializer: ShapeDeserializer { let reader: Reader + let httpResponse: HTTPResponse? + let rawBodyData: Data? public init(data: Data) throws { + self.httpResponse = nil + self.rawBodyData = nil if data.isEmpty { self.reader = Reader() } else { @@ -34,8 +43,22 @@ public struct Deserializer: ShapeDeserializer { } } - init(reader: Reader) { + init(reader: Reader, httpResponse: HTTPResponse? = nil, rawBodyData: Data? = nil) { self.reader = reader + self.httpResponse = httpResponse + self.rawBodyData = rawBodyData + } + + init(httpResponse: HTTPResponse, bodyData: Data) throws { + self.httpResponse = httpResponse + self.rawBodyData = bodyData + if bodyData.isEmpty { + self.reader = Reader() + } else { + // Non-XML bodies (raw blob/string @httpPayload) would fail to parse here; + // in that case we still need a Reader, so fall back to empty. + self.reader = (try? Reader.from(data: bodyData)) ?? Reader() + } } private func targetSchema(_ schema: Schema) -> Schema { @@ -57,6 +80,14 @@ public struct Deserializer: ShapeDeserializer { } for member in structSchema.members { + if let memberDeserializer = try httpBindingDeserializer(for: member) { + do { + try T.readConsumer(member, &value, memberDeserializer) + } catch is DecodedNull { + // skip null + } + continue + } let elementName = xmlElementName(for: member) let isAttribute = member.hasTrait(XmlAttributeTrait.self) let childReader: Reader @@ -75,6 +106,91 @@ public struct Deserializer: ShapeDeserializer { } } + /// If `member` has an HTTP binding trait (httpHeader, httpPrefixHeaders, httpResponseCode, httpPayload) + /// returns a Deserializer backed by the appropriate part of the HTTP response rather than the XML body. + /// Returns nil for normal body-bound members (the XML path should be used). + private func httpBindingDeserializer(for member: Schema) throws -> Deserializer? { + guard let httpResponse else { return nil } + + if let headerTrait = try member.getTrait(HttpHeaderTrait.self) { + guard let headerValue = httpResponse.headers.value(for: headerTrait.value) else { return nil } + // For list-typed headers, split comma-separated values into children. + if member.target?.type == .list || member.type == .list { + let listReader = Reader() + for part in splitHeaderList(headerValue) { + listReader.addChild(Reader(content: part)) + } + return Deserializer(reader: listReader) + } + return Deserializer(reader: Reader(content: headerValue)) + } + + if let prefixTrait = try member.getTrait(HttpPrefixHeadersTrait.self) { + let prefix = prefixTrait.value + let mapReader = Reader() + for (name, value) in httpResponse.headers.dictionary where name.lowercased().hasPrefix(prefix.lowercased()) { + let key = String(name.dropFirst(prefix.count)) + let entry = Reader() + let keyReader = Reader(content: key) + // readMap expects entries where entry[key] and entry[value] can be looked up; + // but our schema-based readMap for headers isn't applicable. Simpler: synthesize + // a flattened map by using member target schema's key/value element names. + // Since headers are always string-to-string(ish), directly build: + // For now, treat as unsupported if headers are missing \u2014 return nil to skip. + _ = (keyReader, entry, value) + } + _ = mapReader + // TODO: full prefix-headers map construction. For now returning nil means the member + // stays unset \u2014 which is still better than throwing "XML could not be parsed". + return nil + } + + if member.hasTrait(HttpResponseCodeTrait.self) { + return Deserializer(reader: Reader(content: String(httpResponse.statusCode.rawValue))) + } + + if member.hasTrait(HttpPayloadTrait.self) { + let targetType = member.target?.type ?? member.type + switch targetType { + case .structure, .union: + // Structure payload: the entire body is the payload struct. Use the root reader. + return Deserializer(reader: reader, httpResponse: httpResponse, rawBodyData: rawBodyData) + case .blob: + guard let rawBodyData else { return nil } + // Reader.readIfPresent() for Data expects base64. For a raw blob payload we need + // the bytes as-is; stash them so readBlob can return them directly. + return Deserializer(reader: Reader(), httpResponse: httpResponse, rawBodyData: rawBodyData) + case .string, .enum: + guard let rawBodyData, let str = String(data: rawBodyData, encoding: .utf8) else { return nil } + return Deserializer(reader: Reader(content: str)) + default: + return nil + } + } + + return nil + } + + /// Splits a comma-separated HTTP header list value per RFC 7230, respecting quoted strings. + private func splitHeaderList(_ value: String) -> [String] { + var result: [String] = [] + var current = "" + var inQuotes = false + for ch in value { + if ch == "\"" { inQuotes.toggle(); current.append(ch); continue } + if ch == "," && !inQuotes { + result.append(current.trimmingCharacters(in: .whitespaces)) + current = "" + continue + } + current.append(ch) + } + if !current.isEmpty { + result.append(current.trimmingCharacters(in: .whitespaces)) + } + return result + } + public func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] { let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) var list = [E]() @@ -194,6 +310,7 @@ public struct Deserializer: ShapeDeserializer { } public func readBlob(_ schema: Schema) throws -> Data { + if let rawBodyData { return rawBodyData } guard let value: Data = try reader.readIfPresent() else { throw XMLDeserializerError("Expected blob for \(schema.id)") } diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index c06a9e7e4..6edbf2a16 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -64,7 +64,7 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { ) async throws -> Output { let bodyData = try await response.body.readData() ?? Data() if (200..<300).contains(response.statusCode.rawValue) { - let deserializer = try codec.makeDeserializer(data: bodyData) + let deserializer = try Deserializer(httpResponse: response, bodyData: bodyData) return try Output.deserialize(deserializer) } else { let errorTypeRegistry = operation.errorTypeRegistry diff --git a/Sources/SmithyXML/Reader/Reader.swift b/Sources/SmithyXML/Reader/Reader.swift index 04e3639d3..be56a0ee3 100644 --- a/Sources/SmithyXML/Reader/Reader.swift +++ b/Sources/SmithyXML/Reader/Reader.swift @@ -30,6 +30,13 @@ public final class Reader: SmithyReader { self.nodeInfo = "" } + /// Creates a Reader backed by a literal string content. Used to adapt non-XML sources + /// (e.g. HTTP header values) into the Reader API so existing read methods work uniformly. + public init(content: String) { + self.nodeInfo = "" + self.content = content + } + /// Used to create a new XML node during reading from XML. /// - Parameter nodeInfo: The node info for this XML node. init(nodeInfo: NodeInfo) { @@ -54,7 +61,7 @@ public final class Reader: SmithyReader { } } - func addChild(_ child: Reader) { + public func addChild(_ child: Reader) { children.append(child) child.parent = self } From 8caa4b746f765cc9ce381be45b9462a9ec740c3a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 20 Apr 2026 17:29:18 -0400 Subject: [PATCH 15/41] fix: Resolve SwiftLint line_length violation in Deserializer --- Sources/SmithyRestXML/Deserializer.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index d4fbfeb19..0bd41d4f8 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -128,7 +128,8 @@ public struct Deserializer: ShapeDeserializer { if let prefixTrait = try member.getTrait(HttpPrefixHeadersTrait.self) { let prefix = prefixTrait.value let mapReader = Reader() - for (name, value) in httpResponse.headers.dictionary where name.lowercased().hasPrefix(prefix.lowercased()) { + let lowerPrefix = prefix.lowercased() + for (name, value) in httpResponse.headers.dictionary where name.lowercased().hasPrefix(lowerPrefix) { let key = String(name.dropFirst(prefix.count)) let entry = Reader() let keyReader = Reader(content: key) From 0616fa4ac9564f9b4527f2718a0756a26e320723 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 20 Apr 2026 17:54:27 -0400 Subject: [PATCH 16/41] fix(SmithyRestXML): Enumerate synthesized children for list-typed HTTP headers readList filters reader.children by XML element name, which correctly matches XML-sourced lists but returns empty for synthetic list Readers built from split header values (children have no XML element name). Add an isHeaderList flag so readList can enumerate all children directly when the list came from an HTTP header. --- Sources/SmithyRestXML/Deserializer.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 0bd41d4f8..15f29188f 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -32,10 +32,15 @@ public struct Deserializer: ShapeDeserializer { let reader: Reader let httpResponse: HTTPResponse? let rawBodyData: Data? + /// Set to true when this Deserializer's Reader holds a list of values synthesized from an + /// HTTP header (each Reader child wraps one comma-split element). In that case, `readList` + /// should enumerate `reader.children` directly without filtering by XML element name. + let isHeaderList: Bool public init(data: Data) throws { self.httpResponse = nil self.rawBodyData = nil + self.isHeaderList = false if data.isEmpty { self.reader = Reader() } else { @@ -43,15 +48,17 @@ public struct Deserializer: ShapeDeserializer { } } - init(reader: Reader, httpResponse: HTTPResponse? = nil, rawBodyData: Data? = nil) { + init(reader: Reader, httpResponse: HTTPResponse? = nil, rawBodyData: Data? = nil, isHeaderList: Bool = false) { self.reader = reader self.httpResponse = httpResponse self.rawBodyData = rawBodyData + self.isHeaderList = isHeaderList } init(httpResponse: HTTPResponse, bodyData: Data) throws { self.httpResponse = httpResponse self.rawBodyData = bodyData + self.isHeaderList = false if bodyData.isEmpty { self.reader = Reader() } else { @@ -120,7 +127,7 @@ public struct Deserializer: ShapeDeserializer { for part in splitHeaderList(headerValue) { listReader.addChild(Reader(content: part)) } - return Deserializer(reader: listReader) + return Deserializer(reader: listReader, isHeaderList: true) } return Deserializer(reader: Reader(content: headerValue)) } @@ -193,6 +200,9 @@ public struct Deserializer: ShapeDeserializer { } public func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] { + if isHeaderList { + return try reader.children.map { try consumer(Deserializer(reader: $0)) } + } let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) var list = [E]() From 8db27d1b56593842bbe9d106f0d0d2f3907e75d9 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 21 Apr 2026 00:29:33 -0400 Subject: [PATCH 17/41] fix(SmithyRestXML): Timestamp-header default format + prefix-headers map 1. HTTP header timestamps default to http-date per Smithy spec (vs .dateTime for XML body). Route through an isFromHttpHeader flag on Deserializer so readTimestamp picks the right default when no @timestampFormat trait is present. 2. HttpPrefixHeadersTrait: replace TODO with real map construction. Synthesize a Reader tree of entry/key/value nodes matching what the existing readMap logic already consumes. Header names that match the prefix (case-insensitive) yield map entries with the stripped suffix as key and comma-joined values. Adds a public Reader(nodeInfo:content:) initializer under the existing SmithyReadWrite SPI to support synthesizing named Reader nodes. --- Sources/SmithyRestXML/Deserializer.swift | 46 ++++++++++++++---------- Sources/SmithyXML/Reader/Reader.swift | 7 ++++ 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 15f29188f..a75be202f 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -36,11 +36,15 @@ public struct Deserializer: ShapeDeserializer { /// HTTP header (each Reader child wraps one comma-split element). In that case, `readList` /// should enumerate `reader.children` directly without filtering by XML element name. let isHeaderList: Bool + /// Set to true when this Deserializer's Reader holds a value from an HTTP header. + /// Changes the default timestamp format from .dateTime (XML) to .httpDate (Smithy HTTP spec). + let isFromHttpHeader: Bool public init(data: Data) throws { self.httpResponse = nil self.rawBodyData = nil self.isHeaderList = false + self.isFromHttpHeader = false if data.isEmpty { self.reader = Reader() } else { @@ -48,17 +52,25 @@ public struct Deserializer: ShapeDeserializer { } } - init(reader: Reader, httpResponse: HTTPResponse? = nil, rawBodyData: Data? = nil, isHeaderList: Bool = false) { + init( + reader: Reader, + httpResponse: HTTPResponse? = nil, + rawBodyData: Data? = nil, + isHeaderList: Bool = false, + isFromHttpHeader: Bool = false + ) { self.reader = reader self.httpResponse = httpResponse self.rawBodyData = rawBodyData self.isHeaderList = isHeaderList + self.isFromHttpHeader = isFromHttpHeader } init(httpResponse: HTTPResponse, bodyData: Data) throws { self.httpResponse = httpResponse self.rawBodyData = bodyData self.isHeaderList = false + self.isFromHttpHeader = false if bodyData.isEmpty { self.reader = Reader() } else { @@ -127,30 +139,23 @@ public struct Deserializer: ShapeDeserializer { for part in splitHeaderList(headerValue) { listReader.addChild(Reader(content: part)) } - return Deserializer(reader: listReader, isHeaderList: true) + return Deserializer(reader: listReader, isHeaderList: true, isFromHttpHeader: true) } - return Deserializer(reader: Reader(content: headerValue)) + return Deserializer(reader: Reader(content: headerValue), isFromHttpHeader: true) } if let prefixTrait = try member.getTrait(HttpPrefixHeadersTrait.self) { let prefix = prefixTrait.value let mapReader = Reader() let lowerPrefix = prefix.lowercased() - for (name, value) in httpResponse.headers.dictionary where name.lowercased().hasPrefix(lowerPrefix) { + for (name, values) in httpResponse.headers.dictionary where name.lowercased().hasPrefix(lowerPrefix) { let key = String(name.dropFirst(prefix.count)) - let entry = Reader() - let keyReader = Reader(content: key) - // readMap expects entries where entry[key] and entry[value] can be looked up; - // but our schema-based readMap for headers isn't applicable. Simpler: synthesize - // a flattened map by using member target schema's key/value element names. - // Since headers are always string-to-string(ish), directly build: - // For now, treat as unsupported if headers are missing \u2014 return nil to skip. - _ = (keyReader, entry, value) + let entry = Reader(nodeInfo: "entry", content: nil) + entry.addChild(Reader(nodeInfo: "key", content: key)) + entry.addChild(Reader(nodeInfo: "value", content: values.joined(separator: ", "))) + mapReader.addChild(entry) } - _ = mapReader - // TODO: full prefix-headers map construction. For now returning nil means the member - // stays unset \u2014 which is still better than throwing "XML could not be parsed". - return nil + return Deserializer(reader: mapReader, isFromHttpHeader: true) } if member.hasTrait(HttpResponseCodeTrait.self) { @@ -201,7 +206,7 @@ public struct Deserializer: ShapeDeserializer { public func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] { if isHeaderList { - return try reader.children.map { try consumer(Deserializer(reader: $0)) } + return try reader.children.map { try consumer(Deserializer(reader: $0, isFromHttpHeader: true)) } } let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) var list = [E]() @@ -329,7 +334,12 @@ public struct Deserializer: ShapeDeserializer { } public func readTimestamp(_ schema: Schema) throws -> Date { - let format = resolveTimestampFormat(schema) + let format: TimestampFormat + if (try? schema.getTrait(TimestampFormatTrait.self)) != nil { + format = resolveTimestampFormat(schema) + } else { + format = isFromHttpHeader ? .httpDate : .dateTime + } guard let value: Date = try reader.readTimestampIfPresent(format: format) else { throw XMLDeserializerError("Expected timestamp for \(schema.id)") } diff --git a/Sources/SmithyXML/Reader/Reader.swift b/Sources/SmithyXML/Reader/Reader.swift index be56a0ee3..9acbcfe3c 100644 --- a/Sources/SmithyXML/Reader/Reader.swift +++ b/Sources/SmithyXML/Reader/Reader.swift @@ -37,6 +37,13 @@ public final class Reader: SmithyReader { self.content = content } + /// Creates a Reader with a given nodeInfo and text content. Used to synthesize + /// Reader trees matching the shape that `readMap` / `readList` expect. + public init(nodeInfo: NodeInfo, content: String?) { + self.nodeInfo = nodeInfo + self.content = content + } + /// Used to create a new XML node during reading from XML. /// - Parameter nodeInfo: The node info for this XML node. init(nodeInfo: NodeInfo) { From 7bd40a0246a0c605e3091031e058a6d9e4349afc Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 21 Apr 2026 13:44:08 -0400 Subject: [PATCH 18/41] fix(SmithyRestXML): Add SmithyCodeGeneratorPlugin, HTTP traits, fix serializer/deserializer --- .../TraitLibrary/AllSupportedTraits.swift | 3 + .../Smithy/TraitLibrary/HttpLabelTrait.swift | 12 +++ .../TraitLibrary/HttpQueryParamsTrait.swift | 12 +++ .../Smithy/TraitLibrary/HttpQueryTrait.swift | 19 ++++ Sources/SmithyRestXML/Deserializer.swift | 30 ++++-- .../SmithyRestXML/HTTPClientProtocol.swift | 2 +- Sources/SmithyRestXML/Serializer.swift | 100 ++++++++++++++++-- .../swift/codegen/PackageManifestGenerator.kt | 18 +++- 8 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 Sources/Smithy/TraitLibrary/HttpLabelTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/HttpQueryTrait.swift diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift index 9d4c591c6..a86aa2ad5 100644 --- a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -27,8 +27,11 @@ private let allSupportedTraitTypes: [ShapeID: any Trait.Type] = [ EventHeaderTrait.id: EventHeaderTrait.self, EventPayloadTrait.id: EventPayloadTrait.self, HttpHeaderTrait.id: HttpHeaderTrait.self, + HttpLabelTrait.id: HttpLabelTrait.self, HttpPayloadTrait.id: HttpPayloadTrait.self, HttpPrefixHeadersTrait.id: HttpPrefixHeadersTrait.self, + HttpQueryTrait.id: HttpQueryTrait.self, + HttpQueryParamsTrait.id: HttpQueryParamsTrait.self, HttpResponseCodeTrait.id: HttpResponseCodeTrait.self, InputTrait.id: InputTrait.self, OutputTrait.id: OutputTrait.self, diff --git a/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift b/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift new file mode 100644 index 000000000..922c87624 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpLabelTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpLabel") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift b/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift new file mode 100644 index 000000000..a6d120a3a --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpQueryParamsTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpQueryParams") } + public var node: Node { [:] } + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift b/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift new file mode 100644 index 000000000..35f92a998 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct HttpQueryTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "httpQuery") } + public let value: String + public var node: Node { .string(value) } + + public init(node: Node) throws { + guard case .string(let value) = node else { + throw TraitError("httpQuery trait requires a string value") + } + self.value = value + } +} diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index a75be202f..77fa0db4d 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -112,6 +112,9 @@ public struct Deserializer: ShapeDeserializer { let childReader: Reader if isAttribute { childReader = reader[NodeInfo(elementName, location: .attribute)] + } else if reader.nodeInfo.name == elementName { + // Unwrapped output: the reader itself IS the member element (e.g. @s3UnwrappedXmlOutput) + childReader = reader } else { childReader = reader[NodeInfo(elementName)] } @@ -135,8 +138,10 @@ public struct Deserializer: ShapeDeserializer { guard let headerValue = httpResponse.headers.value(for: headerTrait.value) else { return nil } // For list-typed headers, split comma-separated values into children. if member.target?.type == .list || member.type == .list { + let memberTarget = member.target?.member.target ?? member.target?.member + let isTimestamp = memberTarget?.type == .timestamp let listReader = Reader() - for part in splitHeaderList(headerValue) { + for part in splitHeaderList(headerValue, isTimestamp: isTimestamp) { listReader.addChild(Reader(content: part)) } return Deserializer(reader: listReader, isHeaderList: true, isFromHttpHeader: true) @@ -167,14 +172,18 @@ public struct Deserializer: ShapeDeserializer { switch targetType { case .structure, .union: // Structure payload: the entire body is the payload struct. Use the root reader. + // If the body is empty, return nil so the member stays nil. + guard reader.hasContent || !reader.children.isEmpty else { return nil } return Deserializer(reader: reader, httpResponse: httpResponse, rawBodyData: rawBodyData) case .blob: - guard let rawBodyData else { return nil } + // Only return a deserializer if there's actual body data. + guard let rawBodyData, !rawBodyData.isEmpty else { return nil } // Reader.readIfPresent() for Data expects base64. For a raw blob payload we need // the bytes as-is; stash them so readBlob can return them directly. return Deserializer(reader: Reader(), httpResponse: httpResponse, rawBodyData: rawBodyData) case .string, .enum: - guard let rawBodyData, let str = String(data: rawBodyData, encoding: .utf8) else { return nil } + guard let rawBodyData, !rawBodyData.isEmpty, + let str = String(data: rawBodyData, encoding: .utf8) else { return nil } return Deserializer(reader: Reader(content: str)) default: return nil @@ -184,15 +193,24 @@ public struct Deserializer: ShapeDeserializer { return nil } - /// Splits a comma-separated HTTP header list value per RFC 7230, respecting quoted strings. - private func splitHeaderList(_ value: String) -> [String] { + /// Splits a comma-separated HTTP header list value per RFC 7230, respecting quoted strings + /// and HTTP date values (which contain a comma, e.g. "Mon, 16 Dec 2019 23:48:18 GMT"). + private func splitHeaderList(_ value: String, isTimestamp: Bool = false) -> [String] { var result: [String] = [] var current = "" var inQuotes = false for ch in value { if ch == "\"" { inQuotes.toggle(); current.append(ch); continue } if ch == "," && !inQuotes { - result.append(current.trimmingCharacters(in: .whitespaces)) + let trimmed = current.trimmingCharacters(in: .whitespaces) + // HTTP dates have the form "DDD, DD MMM YYYY HH:MM:SS GMT". + // The day-of-week abbreviation (3 letters) is followed by a comma. + // Detect this: if the accumulated part is exactly 3 alpha chars, it's a partial date. + if isTimestamp && trimmed.count == 3 && trimmed.allSatisfy(\.isLetter) { + current.append(ch) + continue + } + result.append(trimmed) current = "" continue } diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 6edbf2a16..0d034e7ea 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -103,7 +103,7 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { let errorElement = errorReader.children.first { $0.nodeInfo.name == "Error" } ?? errorReader - specificDeserializer = Deserializer(reader: errorElement) + specificDeserializer = Deserializer(reader: errorElement, httpResponse: response) } let error = try registryEntry.swiftType.deserialize(specificDeserializer) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 767b80072..975fd9b41 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -11,6 +11,13 @@ import enum Smithy.ByteStream import struct Smithy.Schema import protocol Smithy.SmithyDocument import struct Smithy.TimestampFormatTrait +import struct Smithy.HttpHeaderTrait +import struct Smithy.HttpLabelTrait +import struct Smithy.HttpPayloadTrait +import struct Smithy.HttpPrefixHeadersTrait +import struct Smithy.HttpQueryParamsTrait +import struct Smithy.HttpQueryTrait +import struct Smithy.HttpResponseCodeTrait import struct Smithy.XmlAttributeTrait import struct Smithy.XmlFlattenedTrait import struct Smithy.XmlNamespaceTrait @@ -25,11 +32,18 @@ import typealias SmithySerialization.WriteValueConsumer @_spi(SmithyReadWrite) import class SmithyXML.Writer public final class Serializer: ShapeSerializer { - private var rootWriter: Writer? + fileprivate var rootWriter: Writer? + fileprivate var rawBlobData: Data? public init() {} public func writeStruct(_ schema: Schema, _ value: S) throws { + // If any member has @httpPayload, serialize only that member as the root element. + if let payloadMember = schema.members.first(where: { $0.hasTrait(HttpPayloadTrait.self) }) { + let payloadSerializer = PayloadMemberSerializer(parent: self) + try S.writeConsumer(payloadMember, value, payloadSerializer) + return + } let nodeInfo = xmlNodeInfo(for: schema) let writer: Writer if let rootWriter { @@ -65,6 +79,7 @@ public final class Serializer: ShapeSerializer { public var data: Data { get throws { + if let rawBlobData { return rawBlobData } guard let rootWriter else { return Data() } return try rootWriter.data() } @@ -81,6 +96,7 @@ private final class MemberSerializer: ShapeSerializer { } func writeStruct(_ schema: Schema, _ value: S) throws { + guard !isHttpBound(schema) else { return } let child = parent[xmlNodeInfo(for: schema)] let memberSerializer = MemberSerializer(parent: child) for member in schema.members { @@ -89,6 +105,7 @@ private final class MemberSerializer: ShapeSerializer { } func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws { + guard !isHttpBound(schema) else { return } let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) if isFlattened { let nodeInfo = xmlNodeInfo(for: schema) @@ -106,6 +123,7 @@ private final class MemberSerializer: ShapeSerializer { } func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws { + guard !isHttpBound(schema) else { return } let isFlattened = schema.hasTrait(XmlFlattenedTrait.self) let mapSchema = schema.target ?? schema let keyNodeInfo = xmlNodeInfo(for: mapSchema.key) @@ -127,20 +145,22 @@ private final class MemberSerializer: ShapeSerializer { } } - func writeBoolean(_ schema: Schema, _ value: Bool) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeByte(_ schema: Schema, _ value: Int8) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeShort(_ schema: Schema, _ value: Int16) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeInteger(_ schema: Schema, _ value: Int) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeLong(_ schema: Schema, _ value: Int) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeFloat(_ schema: Schema, _ value: Float) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeDouble(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBoolean(_ schema: Schema, _ value: Bool) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeByte(_ schema: Schema, _ value: Int8) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeShort(_ schema: Schema, _ value: Int16) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeInteger(_ schema: Schema, _ value: Int) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeLong(_ schema: Schema, _ value: Int) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeFloat(_ schema: Schema, _ value: Float) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeDouble(_ schema: Schema, _ value: Double) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } func writeBigInteger(_ schema: Schema, _ value: Int64) throws { + guard !isHttpBound(schema) else { return } try parent[xmlNodeInfo(for: schema)].write(String(value)) } - func writeBigDecimal(_ schema: Schema, _ value: Double) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeString(_ schema: Schema, _ value: String) throws { try parent[xmlNodeInfo(for: schema)].write(value) } - func writeBlob(_ schema: Schema, _ value: Data) throws { try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBigDecimal(_ schema: Schema, _ value: Double) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeString(_ schema: Schema, _ value: String) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBlob(_ schema: Schema, _ value: Data) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } func writeTimestamp(_ schema: Schema, _ value: Date) throws { + guard !isHttpBound(schema) else { return } try parent[xmlNodeInfo(for: schema)].writeTimestamp(value, format: resolveTimestampFormat(schema)) } func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { @@ -213,3 +233,61 @@ private func xmlNodeInfo(for schema: Schema) -> NodeInfo { } return NodeInfo(name, location: location, namespaceDef: namespaceDef) } + +/// Returns true if the member schema is bound to an HTTP location other than the body. +private func isHttpBound(_ schema: Schema) -> Bool { + schema.hasTrait(HttpHeaderTrait.self) || + schema.hasTrait(HttpLabelTrait.self) || + schema.hasTrait(HttpQueryTrait.self) || + schema.hasTrait(HttpQueryParamsTrait.self) || + schema.hasTrait(HttpPrefixHeadersTrait.self) || + schema.hasTrait(HttpResponseCodeTrait.self) +} + +/// Serializes an @httpPayload member as the root of the document. +/// For structure/union payloads, the payload value becomes the root XML element. +/// For blob/string payloads, the raw bytes are written directly. +private final class PayloadMemberSerializer: ShapeSerializer { + let outer: Serializer + + init(parent: Serializer) { + self.outer = parent + } + + func writeStruct(_ schema: Schema, _ value: S) throws { + // The payload member's target type becomes the root element + let target = schema.target ?? schema + let writer = Writer(nodeInfo: xmlNodeInfo(for: target)) + outer.rootWriter = writer + let memberSerializer = MemberSerializer(parent: writer) + for member in target.members { + try S.writeConsumer(member, value, memberSerializer) + } + } + + func writeBlob(_ schema: Schema, _ value: Data) throws { + // Raw blob payload — store as-is (not base64-encoded XML) + outer.rawBlobData = value + } + + func writeString(_ schema: Schema, _ value: String) throws { + // String payload — store as UTF-8 bytes + outer.rawBlobData = Data(value.utf8) + } + + func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws {} + func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws {} + func writeBoolean(_ schema: Schema, _ value: Bool) throws {} + func writeByte(_ schema: Schema, _ value: Int8) throws {} + func writeShort(_ schema: Schema, _ value: Int16) throws {} + func writeInteger(_ schema: Schema, _ value: Int) throws {} + func writeLong(_ schema: Schema, _ value: Int) throws {} + func writeFloat(_ schema: Schema, _ value: Float) throws {} + func writeDouble(_ schema: Schema, _ value: Double) throws {} + func writeBigInteger(_ schema: Schema, _ value: Int64) throws {} + func writeBigDecimal(_ schema: Schema, _ value: Double) throws {} + func writeTimestamp(_ schema: Schema, _ value: Date) throws {} + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws {} + func writeNull(_ schema: Schema) throws {} + var data: Data { get throws { Data() } } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt index 393141586..86af01966 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt @@ -6,11 +6,13 @@ package software.amazon.smithy.swift.codegen import software.amazon.smithy.codegen.core.SymbolDependency import software.amazon.smithy.swift.codegen.core.GenerationContext +import software.amazon.smithy.swift.codegen.integration.serde.SerdeUtils class PackageManifestGenerator( val ctx: GenerationContext, ) { fun writePackageManifest(dependencies: List) { + val usesSchemaBased = SerdeUtils.useSchemaBased(ctx.settings, ctx.model) ctx.writerDelegator().useFileWriter("Package.swift") { writer -> writer.write("// swift-tools-version: \$L", ctx.settings.swiftVersion) writer.write("") @@ -48,8 +50,20 @@ class PackageManifestGenerator( writer.openBlock("targets: [", "]") { writer.openBlock(".target(", "),") { writer.write("name: \$S,", ctx.settings.moduleName) - writer.openBlock("dependencies: [", "]") { - dependenciesByTarget.forEach { writeTargetDependency(writer, it) } + if (usesSchemaBased) { + writer.openBlock("dependencies: [", "],") { + dependenciesByTarget.forEach { writeTargetDependency(writer, it) } + } + writer.openBlock("plugins: [", "]") { + writer.openBlock(".plugin(", ")") { + writer.write("name: \"SmithyCodeGeneratorPlugin\",") + writer.write("package: \"smithy-swift\"") + } + } + } else { + writer.openBlock("dependencies: [", "]") { + dependenciesByTarget.forEach { writeTargetDependency(writer, it) } + } } } writer.openBlock(".testTarget(", ")") { From dbf35ef2c438f082401d225443a718f0f9d91155 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 21 Apr 2026 15:09:46 -0400 Subject: [PATCH 19/41] fix(SmithyCodegenCore): Add XmlNameTrait to synthetic input/output shapes for correct XML root element --- .../ModelTransformer/Model+InputOutput.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index 35858609d..eea117dfa 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -10,6 +10,7 @@ import enum Smithy.Prelude import struct Smithy.ShapeID import struct Smithy.TargetsUnitTrait import struct Smithy.TraitCollection +import struct Smithy.XmlNameTrait extension Model { @@ -48,9 +49,21 @@ extension Model { // Add UsedAsInput and UsedAsOutput traits to the input/output structures // These traits allow us to identify inputs/outputs by trait, but allow us to // leave the Smithy input & output traits as set on the original model. - let newInput = newStruct(newID: newInputShapeID, newTraits: [UsedAsInputTrait()], original: inputShape) + // Also add XmlNameTrait with the original shape name so XML serialization uses + // the correct root element name (e.g. "SimpleScalarPropertiesRequest" not "SimpleScalarPropertiesInput"). + var inputExtraTraits = TraitCollection() + inputExtraTraits.add(UsedAsInputTrait()) + if !inputShape.hasTrait(XmlNameTrait.self) && inputShape.id != Prelude.unitSchema.id { + inputExtraTraits.add(try XmlNameTrait(node: .string(inputShape.id.name))) + } + var outputExtraTraits = TraitCollection() + outputExtraTraits.add(UsedAsOutputTrait()) + if !outputShape.hasTrait(XmlNameTrait.self) && outputShape.id != Prelude.unitSchema.id { + outputExtraTraits.add(try XmlNameTrait(node: .string(outputShape.id.name))) + } + let newInput = newStruct(newID: newInputShapeID, newTraits: inputExtraTraits, original: inputShape) let newInputShapeMembers = try renamedMembers(newID: newInputShapeID, original: inputShape) - let newOutput = newStruct(newID: newOutputShapeID, newTraits: [UsedAsOutputTrait()], original: outputShape) + let newOutput = newStruct(newID: newOutputShapeID, newTraits: outputExtraTraits, original: outputShape) let newOutputShapeMembers = try renamedMembers(newID: newOutputShapeID, original: outputShape) // Add the new input & output and their members to the new shape dictionary. From 8620209ebb516204beb892b2e9198a96b53589c6 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 13:03:01 -0400 Subject: [PATCH 20/41] fix: XmlNameTrait inheritance, payload xmlName, service xmlNamespace, empty list/map serialization --- .../ModelTransformer/Model+InputOutput.swift | 9 +++++++++ .../SmithyCodegenCore/Schemas/SchemasCodegen.swift | 6 +++++- Sources/SmithyRestXML/Serializer.swift | 11 +++++++---- Sources/SmithyXML/Writer/Writer.swift | 3 +-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index eea117dfa..61a2f3ad4 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -11,6 +11,7 @@ import struct Smithy.ShapeID import struct Smithy.TargetsUnitTrait import struct Smithy.TraitCollection import struct Smithy.XmlNameTrait +import struct Smithy.XmlNamespaceTrait extension Model { @@ -24,6 +25,11 @@ extension Model { .filter { $0.type == .operation } .compactMap { $0 as? OperationShape } + // Get service-level @xmlNamespace to propagate to synthetic input shapes (for RestXML root element) + let serviceXmlNamespace = shapes.values + .first { $0.type == .service } + .flatMap { try? $0.traits.getTrait(XmlNamespaceTrait.self) } + // Make a copy of this model's shapes to modify var newShapes = shapes @@ -56,6 +62,9 @@ extension Model { if !inputShape.hasTrait(XmlNameTrait.self) && inputShape.id != Prelude.unitSchema.id { inputExtraTraits.add(try XmlNameTrait(node: .string(inputShape.id.name))) } + if let ns = serviceXmlNamespace, !inputShape.hasTrait(XmlNamespaceTrait.self) { + inputExtraTraits.add(ns) + } var outputExtraTraits = TraitCollection() outputExtraTraits.add(UsedAsOutputTrait()) if !outputShape.hasTrait(XmlNameTrait.self) && outputShape.id != Prelude.unitSchema.id { diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift index 39531e6dd..a2436f6be 100644 --- a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -10,6 +10,7 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType import func Smithy.traitType +import struct Smithy.XmlNameTrait /// A generator for the `Schemas.swift` package struct SchemasCodegen { @@ -134,7 +135,10 @@ package struct SchemasCodegen { // Get all the trait IDs that apply to this member & sort let memberTraitIDs = Set(memberShape.traits.traitDict.keys) let targetTraitIDs = Set(try memberShape.target.traits.traitDict.keys) - let allTraitIDs = Array(memberTraitIDs.union(targetTraitIDs)).smithySorted() + // XmlNameTrait on a target shape changes that shape's own root element name, + // but does NOT change the member element name. Only the member's own @xmlName does. + let inheritableTargetTraitIDs = targetTraitIDs.filter { $0 != XmlNameTrait.id } + let allTraitIDs = Array(memberTraitIDs.union(inheritableTargetTraitIDs)).smithySorted() var pairs = [(ShapeID, Node)]() for traitID in allTraitIDs { diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 975fd9b41..f464bf35e 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -114,6 +114,7 @@ private final class MemberSerializer: ShapeSerializer { } } else { let listWriter = parent[xmlNodeInfo(for: schema)] + listWriter.isCollection = true let memberSchema = (schema.target ?? schema).member let memberNodeInfo = xmlNodeInfo(for: memberSchema) for element in value { @@ -137,6 +138,7 @@ private final class MemberSerializer: ShapeSerializer { } } else { let mapWriter = parent[xmlNodeInfo(for: schema)] + mapWriter.isCollection = true for (k, v) in value { let entry = mapWriter[NodeInfo("entry")] try entry[keyNodeInfo].write(k) @@ -255,12 +257,13 @@ private final class PayloadMemberSerializer: ShapeSerializer { } func writeStruct(_ schema: Schema, _ value: S) throws { - // The payload member's target type becomes the root element - let target = schema.target ?? schema - let writer = Writer(nodeInfo: xmlNodeInfo(for: target)) + // Use member's own @xmlName if present; otherwise use the target shape's name/xmlName + let nodeInfo = schema.hasTrait(XmlNameTrait.self) ? xmlNodeInfo(for: schema) : xmlNodeInfo(for: schema.target ?? schema) + let writer = Writer(nodeInfo: nodeInfo) outer.rootWriter = writer + let structSchema = schema.target ?? schema let memberSerializer = MemberSerializer(parent: writer) - for member in target.members { + for member in structSchema.members { try S.writeConsumer(member, value, memberSerializer) } } diff --git a/Sources/SmithyXML/Writer/Writer.swift b/Sources/SmithyXML/Writer/Writer.swift index ee58bb391..3d3d748d8 100644 --- a/Sources/SmithyXML/Writer/Writer.swift +++ b/Sources/SmithyXML/Writer/Writer.swift @@ -27,9 +27,8 @@ public final class Writer: SmithyWriter { var children: [Writer] = [] weak var parent: Writer? let nodeInfo: NodeInfo - var isCollection = false + public var isCollection = false public var nodeInfoPath: [NodeInfo] { (parent?.nodeInfoPath ?? []) + [nodeInfo] } - // MARK: - init & deinit /// Used by the `DocumentWriter` to begin serialization of a model to XML. From 6a8a705e39d4a72da0fab218fbebd624fc947e37 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 13:28:27 -0400 Subject: [PATCH 21/41] fix(SmithyRestXML): Narrow unwrapped-output check to leaf nodes only --- Sources/SmithyRestXML/Deserializer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 77fa0db4d..46dfcf738 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -112,8 +112,8 @@ public struct Deserializer: ShapeDeserializer { let childReader: Reader if isAttribute { childReader = reader[NodeInfo(elementName, location: .attribute)] - } else if reader.nodeInfo.name == elementName { - // Unwrapped output: the reader itself IS the member element (e.g. @s3UnwrappedXmlOutput) + } else if reader.nodeInfo.name == elementName && reader.hasContent && reader.children.isEmpty { + // Unwrapped output: the reader itself IS the member value (e.g. @s3UnwrappedXmlOutput leaf) childReader = reader } else { childReader = reader[NodeInfo(elementName)] From 2f5455fa03446f1d0fe5878ee4782a3584405240 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 14:04:41 -0400 Subject: [PATCH 22/41] fix(SmithyRestXML): Handle streaming @httpPayload without consuming body stream --- Sources/SmithyRestXML/Deserializer.swift | 19 +++++++++++++++++++ .../SmithyRestXML/HTTPClientProtocol.swift | 14 +++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 46dfcf738..c9dd7f9bd 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -7,6 +7,7 @@ import struct Foundation.Data import struct Foundation.Date +import enum Smithy.ByteStream import struct Smithy.Document import struct Smithy.HttpHeaderTrait import struct Smithy.HttpPayloadTrait @@ -32,6 +33,7 @@ public struct Deserializer: ShapeDeserializer { let reader: Reader let httpResponse: HTTPResponse? let rawBodyData: Data? + let bodyStream: ByteStream? /// Set to true when this Deserializer's Reader holds a list of values synthesized from an /// HTTP header (each Reader child wraps one comma-split element). In that case, `readList` /// should enumerate `reader.children` directly without filtering by XML element name. @@ -43,6 +45,7 @@ public struct Deserializer: ShapeDeserializer { public init(data: Data) throws { self.httpResponse = nil self.rawBodyData = nil + self.bodyStream = nil self.isHeaderList = false self.isFromHttpHeader = false if data.isEmpty { @@ -56,12 +59,14 @@ public struct Deserializer: ShapeDeserializer { reader: Reader, httpResponse: HTTPResponse? = nil, rawBodyData: Data? = nil, + bodyStream: ByteStream? = nil, isHeaderList: Bool = false, isFromHttpHeader: Bool = false ) { self.reader = reader self.httpResponse = httpResponse self.rawBodyData = rawBodyData + self.bodyStream = bodyStream self.isHeaderList = isHeaderList self.isFromHttpHeader = isFromHttpHeader } @@ -69,6 +74,7 @@ public struct Deserializer: ShapeDeserializer { init(httpResponse: HTTPResponse, bodyData: Data) throws { self.httpResponse = httpResponse self.rawBodyData = bodyData + self.bodyStream = nil self.isHeaderList = false self.isFromHttpHeader = false if bodyData.isEmpty { @@ -80,6 +86,15 @@ public struct Deserializer: ShapeDeserializer { } } + init(httpResponse: HTTPResponse, bodyStream: ByteStream) { + self.httpResponse = httpResponse + self.rawBodyData = nil + self.bodyStream = bodyStream + self.isHeaderList = false + self.isFromHttpHeader = false + self.reader = Reader() + } + private func targetSchema(_ schema: Schema) -> Schema { schema.target ?? schema } @@ -351,6 +366,10 @@ public struct Deserializer: ShapeDeserializer { return value } + public func readDataStream(_ schema: Schema) throws -> ByteStream { + return bodyStream ?? .data(nil) + } + public func readTimestamp(_ schema: Schema) throws -> Date { let format: TimestampFormat if (try? schema.getTrait(TimestampFormatTrait.self)) != nil { diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 0d034e7ea..706fd0ad0 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -12,8 +12,10 @@ import struct Foundation.Data import enum Smithy.ByteStream import enum Smithy.ClientError import class Smithy.Context +import struct Smithy.HttpPayloadTrait import struct Smithy.Schema import struct Smithy.ShapeID +import struct Smithy.StreamingTrait import struct Smithy.TargetsUnitTrait import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder @@ -62,11 +64,21 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { context: Context, response: HTTPResponse ) async throws -> Output { - let bodyData = try await response.body.readData() ?? Data() if (200..<300).contains(response.statusCode.rawValue) { + // If the output has a streaming @httpPayload member, don't read the body into memory — + // pass the ByteStream directly so it can be streamed to the caller. + let hasStreamingPayload = operation.outputSchema.members.contains { + $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) + } + if hasStreamingPayload { + let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) + return try Output.deserialize(deserializer) + } + let bodyData = try await response.body.readData() ?? Data() let deserializer = try Deserializer(httpResponse: response, bodyData: bodyData) return try Output.deserialize(deserializer) } else { + let bodyData = try await response.body.readData() ?? Data() let errorTypeRegistry = operation.errorTypeRegistry // Parse error response; RestXML errors may be wrapped in element From 8ddf59c5fa41210bd59a2dacac424b0dd709d54b Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:25:57 -0400 Subject: [PATCH 23/41] fix(SmithyRestXML): Detect streaming body by ByteStream case, not schema inspection --- .../SmithyRestXML/HTTPClientProtocol.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 706fd0ad0..efad78cf9 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -65,14 +65,17 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { response: HTTPResponse ) async throws -> Output { if (200..<300).contains(response.statusCode.rawValue) { - // If the output has a streaming @httpPayload member, don't read the body into memory — - // pass the ByteStream directly so it can be streamed to the caller. - let hasStreamingPayload = operation.outputSchema.members.contains { - $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) - } - if hasStreamingPayload { - let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) - return try Output.deserialize(deserializer) + // If the response body is a stream (not already buffered data), check whether the + // output has a streaming @httpPayload member. If so, pass the stream through directly + // without consuming it. + if case .stream = response.body { + let hasStreamingPayload = operation.outputSchema.members.contains { + $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) + } + if hasStreamingPayload { + let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) + return try Output.deserialize(deserializer) + } } let bodyData = try await response.body.readData() ?? Data() let deserializer = try Deserializer(httpResponse: response, bodyData: bodyData) From 88e00fabd7804ccec1dabe01a5c49233a272d134 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:39:13 -0400 Subject: [PATCH 24/41] fix: Resolve swiftlint line_length violations in Serializer.swift --- Sources/SmithyRestXML/Serializer.swift | 54 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index f464bf35e..f701582a7 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -147,20 +147,50 @@ private final class MemberSerializer: ShapeSerializer { } } - func writeBoolean(_ schema: Schema, _ value: Bool) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeByte(_ schema: Schema, _ value: Int8) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeShort(_ schema: Schema, _ value: Int16) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeInteger(_ schema: Schema, _ value: Int) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeLong(_ schema: Schema, _ value: Int) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeFloat(_ schema: Schema, _ value: Float) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeDouble(_ schema: Schema, _ value: Double) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBoolean(_ schema: Schema, _ value: Bool) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeByte(_ schema: Schema, _ value: Int8) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeShort(_ schema: Schema, _ value: Int16) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeInteger(_ schema: Schema, _ value: Int) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeLong(_ schema: Schema, _ value: Int) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeFloat(_ schema: Schema, _ value: Float) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeDouble(_ schema: Schema, _ value: Double) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } func writeBigInteger(_ schema: Schema, _ value: Int64) throws { guard !isHttpBound(schema) else { return } try parent[xmlNodeInfo(for: schema)].write(String(value)) } - func writeBigDecimal(_ schema: Schema, _ value: Double) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeString(_ schema: Schema, _ value: String) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } - func writeBlob(_ schema: Schema, _ value: Data) throws { guard !isHttpBound(schema) else { return }; try parent[xmlNodeInfo(for: schema)].write(value) } + func writeBigDecimal(_ schema: Schema, _ value: Double) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeString(_ schema: Schema, _ value: String) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } + func writeBlob(_ schema: Schema, _ value: Data) throws { + guard !isHttpBound(schema) else { return } + try parent[xmlNodeInfo(for: schema)].write(value) + } func writeTimestamp(_ schema: Schema, _ value: Date) throws { guard !isHttpBound(schema) else { return } try parent[xmlNodeInfo(for: schema)].writeTimestamp(value, format: resolveTimestampFormat(schema)) @@ -258,7 +288,9 @@ private final class PayloadMemberSerializer: ShapeSerializer { func writeStruct(_ schema: Schema, _ value: S) throws { // Use member's own @xmlName if present; otherwise use the target shape's name/xmlName - let nodeInfo = schema.hasTrait(XmlNameTrait.self) ? xmlNodeInfo(for: schema) : xmlNodeInfo(for: schema.target ?? schema) + let nodeInfo = schema.hasTrait(XmlNameTrait.self) + ? xmlNodeInfo(for: schema) + : xmlNodeInfo(for: schema.target ?? schema) let writer = Writer(nodeInfo: nodeInfo) outer.rootWriter = writer let structSchema = schema.target ?? schema From 31ea1feebf7449fc9901749921f0f07d9c50a7eb Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:40:02 -0400 Subject: [PATCH 25/41] fix: Resolve swiftlint sorted_imports violations --- .../ModelTransformer/Model+InputOutput.swift | 2 +- Sources/SmithyRestXML/Serializer.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index 61a2f3ad4..350b64625 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -10,8 +10,8 @@ import enum Smithy.Prelude import struct Smithy.ShapeID import struct Smithy.TargetsUnitTrait import struct Smithy.TraitCollection -import struct Smithy.XmlNameTrait import struct Smithy.XmlNamespaceTrait +import struct Smithy.XmlNameTrait extension Model { diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index f701582a7..2756cfee1 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -8,9 +8,6 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream -import struct Smithy.Schema -import protocol Smithy.SmithyDocument -import struct Smithy.TimestampFormatTrait import struct Smithy.HttpHeaderTrait import struct Smithy.HttpLabelTrait import struct Smithy.HttpPayloadTrait @@ -18,10 +15,13 @@ import struct Smithy.HttpPrefixHeadersTrait import struct Smithy.HttpQueryParamsTrait import struct Smithy.HttpQueryTrait import struct Smithy.HttpResponseCodeTrait +import struct Smithy.Schema +import protocol Smithy.SmithyDocument +import struct Smithy.TimestampFormatTrait import struct Smithy.XmlAttributeTrait import struct Smithy.XmlFlattenedTrait -import struct Smithy.XmlNamespaceTrait import struct Smithy.XmlNameTrait +import struct Smithy.XmlNamespaceTrait import protocol SmithySerialization.SerializableStruct import struct SmithySerialization.SerializerError import protocol SmithySerialization.ShapeSerializer From 882e210abeb76ec1e7c062766b9f9715315ab5a7 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:41:26 -0400 Subject: [PATCH 26/41] fix: Resolve remaining swiftlint sorted_imports violation in Serializer.swift --- Sources/SmithyRestXML/Serializer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 2756cfee1..9e1c0d9c1 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -20,8 +20,8 @@ import protocol Smithy.SmithyDocument import struct Smithy.TimestampFormatTrait import struct Smithy.XmlAttributeTrait import struct Smithy.XmlFlattenedTrait -import struct Smithy.XmlNameTrait import struct Smithy.XmlNamespaceTrait +import struct Smithy.XmlNameTrait import protocol SmithySerialization.SerializableStruct import struct SmithySerialization.SerializerError import protocol SmithySerialization.ShapeSerializer From 6d9557051827c04b2546c7f378cb51694ab265fe Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:48:29 -0400 Subject: [PATCH 27/41] fix(SmithyRestXML): Restore schema-based streaming payload detection --- .../SmithyRestXML/HTTPClientProtocol.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index efad78cf9..eebcc0ead 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -16,8 +16,7 @@ import struct Smithy.HttpPayloadTrait import struct Smithy.Schema import struct Smithy.ShapeID import struct Smithy.StreamingTrait -import struct Smithy.TargetsUnitTrait -import class SmithyHTTPAPI.HTTPRequest +import struct Smithy.TargetsUnitTraitimport class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse import protocol SmithySerialization.ClientProtocol @@ -65,17 +64,14 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { response: HTTPResponse ) async throws -> Output { if (200..<300).contains(response.statusCode.rawValue) { - // If the response body is a stream (not already buffered data), check whether the - // output has a streaming @httpPayload member. If so, pass the stream through directly - // without consuming it. - if case .stream = response.body { - let hasStreamingPayload = operation.outputSchema.members.contains { - $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) - } - if hasStreamingPayload { - let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) - return try Output.deserialize(deserializer) - } + // Check if the output has a streaming @httpPayload member (e.g. S3 GetObject body). + // If so, pass the ByteStream through without consuming it. + let hasStreamingPayload = operation.outputSchema.members.contains { + $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) + } + if hasStreamingPayload { + let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) + return try Output.deserialize(deserializer) } let bodyData = try await response.body.readData() ?? Data() let deserializer = try Deserializer(httpResponse: response, bodyData: bodyData) From 7e213752ae5fb8bedd859ef273ab2f170ffd358a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 15:49:00 -0400 Subject: [PATCH 28/41] fix: Repair merged import line in HTTPClientProtocol.swift --- Sources/SmithyRestXML/HTTPClientProtocol.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index eebcc0ead..c678c1a57 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -16,7 +16,8 @@ import struct Smithy.HttpPayloadTrait import struct Smithy.Schema import struct Smithy.ShapeID import struct Smithy.StreamingTrait -import struct Smithy.TargetsUnitTraitimport class SmithyHTTPAPI.HTTPRequest +import struct Smithy.TargetsUnitTrait +import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse import protocol SmithySerialization.ClientProtocol From 308b5d00c7deeb278b1ace4e427982e597351f97 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 22 Apr 2026 16:00:23 -0400 Subject: [PATCH 29/41] test: Update PackageManifestGeneratorTests for new plugin in schema-based target --- .../manifestanddocs/PackageManifestGeneratorTests.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt index 48ce8d8bc..85bfa098a 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt @@ -77,6 +77,12 @@ class PackageManifestGeneratorTests { name: "ClientRuntime", package: "smithy-swift" ), + ], + plugins: [ + .plugin( + name: "SmithyCodeGeneratorPlugin", + package: "smithy-swift" + ) ] ), .testTarget( From 87bf42f3d64cbe823b0d8902375d10dc61ee9176 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 23 Apr 2026 13:59:30 -0400 Subject: [PATCH 30/41] fix(SmithyRestXML): Handle streaming @httpPayload in Deserializer and HTTPClientProtocol --- Sources/SmithyRestXML/Deserializer.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index c9dd7f9bd..14fb95f2a 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -191,6 +191,10 @@ public struct Deserializer: ShapeDeserializer { guard reader.hasContent || !reader.children.isEmpty else { return nil } return Deserializer(reader: reader, httpResponse: httpResponse, rawBodyData: rawBodyData) case .blob: + // Streaming blob payload: pass the body stream through. + if let bodyStream { + return Deserializer(reader: Reader(), httpResponse: httpResponse, bodyStream: bodyStream) + } // Only return a deserializer if there's actual body data. guard let rawBodyData, !rawBodyData.isEmpty else { return nil } // Reader.readIfPresent() for Data expects base64. For a raw blob payload we need From 609c3e147c660ac9b4953576d9b10ccd884ac9a6 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 23 Apr 2026 18:44:18 +0000 Subject: [PATCH 31/41] fix(SmithyRestXML): Handle streaming @httpPayload in serialization and improve deserialization detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. Serialization: The Serializer's PayloadMemberSerializer did not implement writeDataStream(), so streaming blob payloads (e.g. S3 PutObject body) were silently dropped — the request body was empty. Added writeDataStream() to capture the ByteStream, and updated serializeRequest() to use it as the request body directly instead of the serialized XML data. 2. Deserialization: The hasStreamingPayload check now also checks member.hasTrait(StreamingTrait.self) in addition to member.target?.hasTrait(StreamingTrait.self), since SchemasCodegen merges target traits into member schemas. --- .../SmithyRestXML/HTTPClientProtocol.swift | 19 +++++++++++++------ Sources/SmithyRestXML/Serializer.swift | 6 ++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index c678c1a57..e1502026c 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -52,11 +52,14 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { return } - let serializer = try codec.makeSerializer() + let serializer = Serializer() try input.serialize(serializer) - let data = try serializer.data - let body = ByteStream.data(data) - requestBuilder.withBody(body) + if let streamingBody = serializer.streamingBody { + requestBuilder.withBody(streamingBody) + } else { + let data = try serializer.data + requestBuilder.withBody(.data(data)) + } } public func deserializeResponse( @@ -67,8 +70,12 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { if (200..<300).contains(response.statusCode.rawValue) { // Check if the output has a streaming @httpPayload member (e.g. S3 GetObject body). // If so, pass the ByteStream through without consuming it. - let hasStreamingPayload = operation.outputSchema.members.contains { - $0.hasTrait(HttpPayloadTrait.self) && ($0.target?.hasTrait(StreamingTrait.self) ?? false) + // Check both the member's own traits (which inherit from the target) and the + // target's traits directly, to be resilient to trait resolution differences. + let hasStreamingPayload = operation.outputSchema.members.contains { member in + guard member.hasTrait(HttpPayloadTrait.self) else { return false } + return member.hasTrait(StreamingTrait.self) + || (member.target?.hasTrait(StreamingTrait.self) ?? false) } if hasStreamingPayload { let deserializer = Deserializer(httpResponse: response, bodyStream: response.body) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 9e1c0d9c1..123bbd123 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -34,6 +34,7 @@ import typealias SmithySerialization.WriteValueConsumer public final class Serializer: ShapeSerializer { fileprivate var rootWriter: Writer? fileprivate var rawBlobData: Data? + var streamingBody: ByteStream? public init() {} @@ -305,6 +306,11 @@ private final class PayloadMemberSerializer: ShapeSerializer { outer.rawBlobData = value } + func writeDataStream(_ schema: Schema, _ value: ByteStream) throws { + // Streaming blob payload — pass through as-is + outer.streamingBody = value + } + func writeString(_ schema: Schema, _ value: String) throws { // String payload — store as UTF-8 bytes outer.rawBlobData = Data(value.utf8) From 3d21fdd148e4485e1f93527a55479b39008a50dd Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 23 Apr 2026 23:38:56 -0400 Subject: [PATCH 32/41] fix(SmithyRestXML): Handle empty-body 404 as NotFound error for S3 HeadObject --- Sources/SmithyRestXML/HTTPClientProtocol.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index e1502026c..30d639e06 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -110,6 +110,12 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { registryEntry = errorTypeRegistry.find { entry in entry.schema.id.name == code } + } else if bodyData.isEmpty && response.statusCode == .notFound { + // S3 customization: HEAD on nonexistent object returns 404 with empty body. + // Match NotFound error by name when body is empty and status is 404. + registryEntry = errorTypeRegistry.find { entry in + entry.schema.id.name == "NotFound" + } } else { registryEntry = nil } From f4b16767332d78111a792da306aa42d005ac4734 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 24 Apr 2026 00:04:28 -0400 Subject: [PATCH 33/41] fix: Add @_spi(SchemaBasedSerde) to all trait and SmithyRestXML public types --- .../Smithy/TraitLibrary/HttpHeaderTrait.swift | 1 + .../Smithy/TraitLibrary/HttpLabelTrait.swift | 1 + .../TraitLibrary/HttpPayloadTrait.swift | 1 + .../TraitLibrary/HttpPrefixHeadersTrait.swift | 1 + .../TraitLibrary/HttpQueryParamsTrait.swift | 1 + .../Smithy/TraitLibrary/HttpQueryTrait.swift | 1 + .../TraitLibrary/HttpResponseCodeTrait.swift | 1 + .../TraitLibrary/XmlAttributeTrait.swift | 1 + .../TraitLibrary/XmlFlattenedTrait.swift | 1 + .../Smithy/TraitLibrary/XmlNameTrait.swift | 1 + .../TraitLibrary/XmlNamespaceTrait.swift | 1 + Sources/SmithyRestXML/BaseError.swift | 5 +++++ Sources/SmithyRestXML/Codec.swift | 4 ++++ Sources/SmithyRestXML/Deserializer.swift | 15 +++++++++++++++ .../SmithyRestXML/HTTPClientProtocol.swift | 11 +++++++++++ Sources/SmithyRestXML/Plugin.swift | 3 +++ Sources/SmithyRestXML/Serializer.swift | 19 +++++++++++++++++++ Sources/SmithyRestXML/TimestampUtils.swift | 2 ++ 18 files changed, 70 insertions(+) diff --git a/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift b/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift index d14162ed5..28a350b94 100644 --- a/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpHeaderTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpHeaderTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpHeader") } public let value: String diff --git a/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift b/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift index 922c87624..e77b0b517 100644 --- a/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpLabelTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpLabelTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpLabel") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift b/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift index 23e32bc9f..7e46001cd 100644 --- a/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpPayloadTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpPayloadTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpPayload") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift b/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift index 304bbdf9c..777e133a4 100644 --- a/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpPrefixHeadersTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpPrefixHeadersTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpPrefixHeaders") } public let value: String diff --git a/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift b/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift index a6d120a3a..1cf38847c 100644 --- a/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpQueryParamsTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpQueryParamsTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpQueryParams") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift b/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift index 35f92a998..58cac6413 100644 --- a/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpQueryTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpQueryTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpQuery") } public let value: String diff --git a/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift b/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift index a25aa80ae..edaa81a90 100644 --- a/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift +++ b/Sources/Smithy/TraitLibrary/HttpResponseCodeTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct HttpResponseCodeTrait: Trait { public static var id: ShapeID { .init("smithy.api", "httpResponseCode") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift b/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift index f246cf002..d392c2807 100644 --- a/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift +++ b/Sources/Smithy/TraitLibrary/XmlAttributeTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct XmlAttributeTrait: Trait { public static var id: ShapeID { .init("smithy.api", "xmlAttribute") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift b/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift index a3cc73279..425df4dfc 100644 --- a/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift +++ b/Sources/Smithy/TraitLibrary/XmlFlattenedTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct XmlFlattenedTrait: Trait { public static var id: ShapeID { .init("smithy.api", "xmlFlattened") } public var node: Node { [:] } diff --git a/Sources/Smithy/TraitLibrary/XmlNameTrait.swift b/Sources/Smithy/TraitLibrary/XmlNameTrait.swift index ead35a7e9..4efce68f7 100644 --- a/Sources/Smithy/TraitLibrary/XmlNameTrait.swift +++ b/Sources/Smithy/TraitLibrary/XmlNameTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct XmlNameTrait: Trait { public static var id: ShapeID { .init("smithy.api", "xmlName") } public let value: String diff --git a/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift b/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift index 97cea412c..32153e098 100644 --- a/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift +++ b/Sources/Smithy/TraitLibrary/XmlNamespaceTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) public struct XmlNamespaceTrait: Trait { public static var id: ShapeID { .init("smithy.api", "xmlNamespace") } public let uri: String diff --git a/Sources/SmithyRestXML/BaseError.swift b/Sources/SmithyRestXML/BaseError.swift index 9314c13a2..803449ce2 100644 --- a/Sources/SmithyRestXML/BaseError.swift +++ b/Sources/SmithyRestXML/BaseError.swift @@ -6,12 +6,17 @@ // import enum Smithy.Prelude +@_spi(SchemaBasedSerde) import struct Smithy.Schema +@_spi(SchemaBasedSerde) import protocol SmithySerialization.DeserializableStruct +@_spi(SchemaBasedSerde) import typealias SmithySerialization.ReadStructConsumer +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeDeserializer @_spi(RestXML) +@_spi(SchemaBasedSerde) public struct BaseError { public var code: String? public var message: String? diff --git a/Sources/SmithyRestXML/Codec.swift b/Sources/SmithyRestXML/Codec.swift index d077cc957..8940817e2 100644 --- a/Sources/SmithyRestXML/Codec.swift +++ b/Sources/SmithyRestXML/Codec.swift @@ -6,10 +6,14 @@ // import struct Foundation.Data +@_spi(SchemaBasedSerde) import protocol SmithySerialization.Codec +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeDeserializer +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeSerializer +@_spi(SchemaBasedSerde) public struct Codec: SmithySerialization.Codec { public init() {} diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 14fb95f2a..778a0cbfa 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -9,26 +9,41 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream import struct Smithy.Document +@_spi(SchemaBasedSerde) import struct Smithy.HttpHeaderTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpPayloadTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpPrefixHeadersTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpResponseCodeTrait +@_spi(SchemaBasedSerde) import struct Smithy.Schema +@_spi(SchemaBasedSerde) import struct Smithy.TimestampFormatTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlAttributeTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlFlattenedTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlNameTrait import class SmithyHTTPAPI.HTTPResponse +@_spi(SchemaBasedSerde) import protocol SmithySerialization.DeserializableStruct +@_spi(SchemaBasedSerde) import typealias SmithySerialization.ReadStructConsumer +@_spi(SchemaBasedSerde) import typealias SmithySerialization.ReadValueConsumer +@_spi(SchemaBasedSerde) import struct SmithySerialization.SerializerError +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeDeserializer @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat @_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter @_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo @_spi(SmithyReadWrite) import class SmithyXML.Reader +@_spi(SchemaBasedSerde) public struct Deserializer: ShapeDeserializer { let reader: Reader let httpResponse: HTTPResponse? diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 30d639e06..2e6169562 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -12,22 +12,33 @@ import struct Foundation.Data import enum Smithy.ByteStream import enum Smithy.ClientError import class Smithy.Context +@_spi(SchemaBasedSerde) import struct Smithy.HttpPayloadTrait +@_spi(SchemaBasedSerde) import struct Smithy.Schema +@_spi(SchemaBasedSerde) import struct Smithy.ShapeID +@_spi(SchemaBasedSerde) import struct Smithy.StreamingTrait +@_spi(SchemaBasedSerde) import struct Smithy.TargetsUnitTrait import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ClientProtocol +@_spi(SchemaBasedSerde) import protocol SmithySerialization.Codec +@_spi(SchemaBasedSerde) import protocol SmithySerialization.DeserializableStruct +@_spi(SchemaBasedSerde) import struct SmithySerialization.Operation +@_spi(SchemaBasedSerde) import struct SmithySerialization.TypeRegistry @_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo @_spi(SmithyReadWrite) import class SmithyXML.Reader +@_spi(SchemaBasedSerde) public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { public typealias RequestType = HTTPRequest public typealias ResponseType = HTTPResponse diff --git a/Sources/SmithyRestXML/Plugin.swift b/Sources/SmithyRestXML/Plugin.swift index f88872b0d..000c94f58 100644 --- a/Sources/SmithyRestXML/Plugin.swift +++ b/Sources/SmithyRestXML/Plugin.swift @@ -9,9 +9,12 @@ import ClientRuntime import Smithy import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse +@_spi(SchemaBasedSerde) import protocol SmithySerialization.DeserializableStruct +@_spi(SchemaBasedSerde) import protocol SmithySerialization.SerializableStruct +@_spi(SchemaBasedSerde) public struct Plugin: ClientRuntime.Plugin { public init() {} diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 123bbd123..844fc52e3 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -8,29 +8,48 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream +@_spi(SchemaBasedSerde) import struct Smithy.HttpHeaderTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpLabelTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpPayloadTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpPrefixHeadersTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpQueryParamsTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpQueryTrait +@_spi(SchemaBasedSerde) import struct Smithy.HttpResponseCodeTrait +@_spi(SchemaBasedSerde) import struct Smithy.Schema +@_spi(SchemaBasedSerde) import protocol Smithy.SmithyDocument +@_spi(SchemaBasedSerde) import struct Smithy.TimestampFormatTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlAttributeTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlFlattenedTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlNamespaceTrait +@_spi(SchemaBasedSerde) import struct Smithy.XmlNameTrait +@_spi(SchemaBasedSerde) import protocol SmithySerialization.SerializableStruct +@_spi(SchemaBasedSerde) import struct SmithySerialization.SerializerError +@_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeSerializer +@_spi(SchemaBasedSerde) import typealias SmithySerialization.WriteValueConsumer @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat @_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter @_spi(SmithyReadWrite) import struct SmithyXML.NodeInfo @_spi(SmithyReadWrite) import class SmithyXML.Writer +@_spi(SchemaBasedSerde) public final class Serializer: ShapeSerializer { fileprivate var rootWriter: Writer? fileprivate var rawBlobData: Data? diff --git a/Sources/SmithyRestXML/TimestampUtils.swift b/Sources/SmithyRestXML/TimestampUtils.swift index 535da1f50..3dca8f7b5 100644 --- a/Sources/SmithyRestXML/TimestampUtils.swift +++ b/Sources/SmithyRestXML/TimestampUtils.swift @@ -5,7 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 // +@_spi(SchemaBasedSerde) import struct Smithy.Schema +@_spi(SchemaBasedSerde) import struct Smithy.TimestampFormatTrait @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat From d2ce4f8ee2a1bd4fb042c0b1b436b949367b20d0 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 24 Apr 2026 00:10:03 -0400 Subject: [PATCH 34/41] fix: Add SchemaBasedSerde SPI to SmithyRestXMLTypes.kt --- .../smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt index ab358703b..6d580cef5 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyRestXMLTypes.kt @@ -13,7 +13,7 @@ private fun runtimeSymbol( name: String, declaration: SwiftDeclaration?, additionalImports: List = emptyList(), - spiName: List = emptyList(), + spiName: List = listOf("SchemaBasedSerde"), ): Symbol = SwiftSymbol.make( name, From dcb0aef4ce6c6f376cb94178ec407c88ef291c5b Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 24 Apr 2026 12:32:42 -0400 Subject: [PATCH 35/41] fix: Render empty struct XML elements and remove unused import --- Sources/SmithyRestXML/Plugin.swift | 1 - Sources/SmithyRestXML/Serializer.swift | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyRestXML/Plugin.swift b/Sources/SmithyRestXML/Plugin.swift index 000c94f58..198249667 100644 --- a/Sources/SmithyRestXML/Plugin.swift +++ b/Sources/SmithyRestXML/Plugin.swift @@ -6,7 +6,6 @@ // import ClientRuntime -import Smithy import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse @_spi(SchemaBasedSerde) diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index 844fc52e3..f67395fef 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -118,6 +118,7 @@ private final class MemberSerializer: ShapeSerializer { func writeStruct(_ schema: Schema, _ value: S) throws { guard !isHttpBound(schema) else { return } let child = parent[xmlNodeInfo(for: schema)] + child.isCollection = true let memberSerializer = MemberSerializer(parent: child) for member in schema.members { try S.writeConsumer(member, value, memberSerializer) From a35d4fa005fd633927c0904e7b95c4cdf44ebaa9 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 24 Apr 2026 13:49:33 -0400 Subject: [PATCH 36/41] fix: Handle event stream responses in RestXML deserialization --- Package.swift | 1 + Sources/SmithyRestXML/HTTPClientProtocol.swift | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Package.swift b/Package.swift index 2f21b46d0..6c8ee4f34 100644 --- a/Package.swift +++ b/Package.swift @@ -321,6 +321,7 @@ let package = Package( dependencies: [ "ClientRuntime", "Smithy", + "SmithyEventStreams", "SmithySerialization", "SmithyXML", "SmithyTimestamps", diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 2e6169562..135aa8d2e 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -22,6 +22,8 @@ import struct Smithy.ShapeID import struct Smithy.StreamingTrait @_spi(SchemaBasedSerde) import struct Smithy.TargetsUnitTrait +@_spi(SchemaBasedSerde) +import struct SmithyEventStreams.EventStreamDeserializer import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPRequestBuilder import class SmithyHTTPAPI.HTTPResponse @@ -79,6 +81,12 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { response: HTTPResponse ) async throws -> Output { if (200..<300).contains(response.statusCode.rawValue) { + // Check if the response is an event stream (e.g. S3 SelectObjectContent). + // If so, use the EventStreamDeserializer which knows how to decode the stream. + if response.headers.value(for: "Content-Type")?.contains("application/vnd.amazon.eventstream") ?? false { + let eventStreamDeserializer = EventStreamDeserializer(codec: codec, response: response) + return try Output.deserialize(eventStreamDeserializer) + } // Check if the output has a streaming @httpPayload member (e.g. S3 GetObject body). // If so, pass the ByteStream through without consuming it. // Check both the member's own traits (which inherit from the target) and the From 3c715a27e289880ecd72db52abe94e741aa92145 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 24 Apr 2026 15:33:40 -0400 Subject: [PATCH 37/41] fix: Detect event streams via schema instead of content-type --- Sources/SmithyRestXML/HTTPClientProtocol.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 135aa8d2e..62013d49c 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -81,9 +81,14 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { response: HTTPResponse ) async throws -> Output { if (200..<300).contains(response.statusCode.rawValue) { - // Check if the response is an event stream (e.g. S3 SelectObjectContent). - // If so, use the EventStreamDeserializer which knows how to decode the stream. - if response.headers.value(for: "Content-Type")?.contains("application/vnd.amazon.eventstream") ?? false { + // Check if the output has a streaming event stream @httpPayload member + // (e.g. S3 SelectObjectContent). If so, use EventStreamDeserializer. + let hasEventStreamPayload = operation.outputSchema.members.contains { member in + guard member.hasTrait(HttpPayloadTrait.self) else { return false } + guard member.hasTrait(StreamingTrait.self) || (member.target?.hasTrait(StreamingTrait.self) ?? false) else { return false } + return member.type == .union + } + if hasEventStreamPayload { let eventStreamDeserializer = EventStreamDeserializer(codec: codec, response: response) return try Output.deserialize(eventStreamDeserializer) } From efaceec5c6f063da4d3be2b9ab9650fcd4743637 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 27 Apr 2026 15:10:00 -0400 Subject: [PATCH 38/41] fix: RestXML event stream target + error resolver hook --- .../SmithyRestXML/HTTPClientProtocol.swift | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 62013d49c..c73cf6bfb 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -49,8 +49,29 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { public let codec: SmithySerialization.Codec = Codec() public let noErrorWrapping: Bool - public init(noErrorWrapping: Bool = false) { + /// Optional hook called for non-2xx responses before the generic error path. + /// If it returns an error, that error is thrown. If it returns nil, the generic + /// TypeRegistry-based error matching runs as normal. + /// Signature: (HTTPResponse, Data, TypeRegistry, Bool) async throws -> Error? + public let customErrorResolver: ( + @Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)? + )? + + /// Optional hook called after a modeled error is deserialized from the TypeRegistry. + /// Allows setting additional properties (e.g. requestID2) on the error before it is thrown. + /// Signature: (inout ServiceError & HTTPError & Error, HTTPResponse) -> Void + public let errorPostProcessor: ( + @Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void + )? + + public init( + noErrorWrapping: Bool = false, + customErrorResolver: (@Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)?)? = nil, + errorPostProcessor: (@Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void)? = nil + ) { self.noErrorWrapping = noErrorWrapping + self.customErrorResolver = customErrorResolver + self.errorPostProcessor = errorPostProcessor } public func serializeRequest( @@ -85,8 +106,10 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { // (e.g. S3 SelectObjectContent). If so, use EventStreamDeserializer. let hasEventStreamPayload = operation.outputSchema.members.contains { member in guard member.hasTrait(HttpPayloadTrait.self) else { return false } - guard member.hasTrait(StreamingTrait.self) || (member.target?.hasTrait(StreamingTrait.self) ?? false) else { return false } - return member.type == .union + guard member.hasTrait(StreamingTrait.self) + || (member.target?.hasTrait(StreamingTrait.self) ?? false) + else { return false } + return (member.target ?? member).type == .union } if hasEventStreamPayload { let eventStreamDeserializer = EventStreamDeserializer(codec: codec, response: response) @@ -112,6 +135,14 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { let bodyData = try await response.body.readData() ?? Data() let errorTypeRegistry = operation.errorTypeRegistry + // If a custom error resolver is set, try it first. + if let customErrorResolver, + let resolvedError = try await customErrorResolver( + response, bodyData, errorTypeRegistry, noErrorWrapping + ) { + throw resolvedError + } + // Parse error response; RestXML errors may be wrapped in element let errorDeserializer = try Deserializer(data: bodyData) let errorReader = errorDeserializer.reader @@ -163,6 +194,7 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { } modeledError.message = baseError.message modeledError.httpResponse = response + errorPostProcessor?(&modeledError, response) throw modeledError } else { throw UnknownHTTPServiceError( From 3281048049309ef0e4a5a0d1e58b69e5b5606804 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 27 Apr 2026 15:53:54 -0400 Subject: [PATCH 39/41] fix: skip XML parsing for blob event payloads --- .../EventContentDeserializer.swift | 15 ++++- .../Deserializers/RawBlobDeserializer.swift | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift diff --git a/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift b/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift index 11a2824e3..94a41317a 100644 --- a/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift +++ b/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift @@ -32,11 +32,20 @@ struct EventContentDeserializer: ShapeDeserializer { // Deserialize the event payload, to the member marked with @eventPayload if it exists, // to the structure's members otherwise. - // Use a deserializer for the protocol in use, by making it from the codec. - let payloadDeserializer = try codec.makeDeserializer(data: message.payload) if let payloadMember = schema.members.first(where: { $0.hasTrait(EventPayloadTrait.self) }) { - try T.readConsumer(payloadMember, &value, payloadDeserializer) + // For blob @eventPayload members, return the raw bytes directly without + // parsing through the codec (which would try to parse as XML/JSON). + if payloadMember.type == .blob { + let blobDeserializer = RawBlobDeserializer(data: message.payload) + try T.readConsumer(payloadMember, &value, blobDeserializer) + } else { + // For structured @eventPayload members, use the protocol codec. + let payloadDeserializer = try codec.makeDeserializer(data: message.payload) + try T.readConsumer(payloadMember, &value, payloadDeserializer) + } } else { + // No @eventPayload member — deserialize the whole structure from the payload. + let payloadDeserializer = try codec.makeDeserializer(data: message.payload) try payloadDeserializer.readStruct(schema, &value) } diff --git a/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift new file mode 100644 index 000000000..edfa80248 --- /dev/null +++ b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import struct Smithy.Document +@_spi(SchemaBasedSerde) +import struct Smithy.Schema +@_spi(SchemaBasedSerde) +import protocol SmithySerialization.DeserializableStruct +import struct SmithySerialization.SerializerError +@_spi(SchemaBasedSerde) +import protocol SmithySerialization.ShapeDeserializer + +/// A minimal deserializer that returns raw bytes for blob reads. +/// +/// Used for `@eventPayload` members of blob type, where the event payload +/// should be returned as-is without protocol-specific parsing (e.g. XML). +struct RawBlobDeserializer: ShapeDeserializer { + let data: Data + + func readBlob(_ schema: Schema) throws -> Data { + data + } + + func readStruct(_ schema: Schema, _ value: inout T) throws where T: SmithySerialization.DeserializableStruct { + throw notImplemented + } + + func readList(_ schema: Schema, _ consumer: (any ShapeDeserializer) throws -> E) throws -> [E] { + throw notImplemented + } + + func readMap(_ schema: Schema, _ consumer: (any ShapeDeserializer) throws -> V) throws -> [String: V] { + throw notImplemented + } + + func readBoolean(_ schema: Schema) throws -> Bool { throw notImplemented } + func readByte(_ schema: Schema) throws -> Int8 { throw notImplemented } + func readShort(_ schema: Schema) throws -> Int16 { throw notImplemented } + func readInteger(_ schema: Schema) throws -> Int { throw notImplemented } + func readLong(_ schema: Schema) throws -> Int { throw notImplemented } + func readFloat(_ schema: Schema) throws -> Float { throw notImplemented } + func readDouble(_ schema: Schema) throws -> Double { throw notImplemented } + func readBigInteger(_ schema: Schema) throws -> Int64 { throw notImplemented } + func readBigDecimal(_ schema: Schema) throws -> Double { throw notImplemented } + func readString(_ schema: Schema) throws -> String { throw notImplemented } + func readDocument(_ schema: Schema) throws -> Document { throw notImplemented } + func readTimestamp(_ schema: Schema) throws -> Date { throw notImplemented } + func readNull(_ schema: Schema) throws -> T? { throw notImplemented } + func isNull() throws -> Bool { throw notImplemented } + var containerSize: Int { -1 } + + private var notImplemented: SerializerError { .init("Not implemented") } +} From b7bd655369d9719602ded48b3aad61fe44ea8b61 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 27 Apr 2026 17:54:18 -0400 Subject: [PATCH 40/41] fix: style consistency in RawBlobDeserializer + error message --- .../Deserializers/RawBlobDeserializer.swift | 72 +++++++++++++++---- .../SmithyRestXML/HTTPClientProtocol.swift | 1 + 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift index edfa80248..dec9f2e5f 100644 --- a/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift +++ b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift @@ -27,7 +27,7 @@ struct RawBlobDeserializer: ShapeDeserializer { data } - func readStruct(_ schema: Schema, _ value: inout T) throws where T: SmithySerialization.DeserializableStruct { + func readStruct(_ schema: Schema, _ value: inout T) throws where T: DeserializableStruct { throw notImplemented } @@ -39,20 +39,62 @@ struct RawBlobDeserializer: ShapeDeserializer { throw notImplemented } - func readBoolean(_ schema: Schema) throws -> Bool { throw notImplemented } - func readByte(_ schema: Schema) throws -> Int8 { throw notImplemented } - func readShort(_ schema: Schema) throws -> Int16 { throw notImplemented } - func readInteger(_ schema: Schema) throws -> Int { throw notImplemented } - func readLong(_ schema: Schema) throws -> Int { throw notImplemented } - func readFloat(_ schema: Schema) throws -> Float { throw notImplemented } - func readDouble(_ schema: Schema) throws -> Double { throw notImplemented } - func readBigInteger(_ schema: Schema) throws -> Int64 { throw notImplemented } - func readBigDecimal(_ schema: Schema) throws -> Double { throw notImplemented } - func readString(_ schema: Schema) throws -> String { throw notImplemented } - func readDocument(_ schema: Schema) throws -> Document { throw notImplemented } - func readTimestamp(_ schema: Schema) throws -> Date { throw notImplemented } - func readNull(_ schema: Schema) throws -> T? { throw notImplemented } - func isNull() throws -> Bool { throw notImplemented } + func readBoolean(_ schema: Schema) throws -> Bool { + throw notImplemented + } + + func readByte(_ schema: Schema) throws -> Int8 { + throw notImplemented + } + + func readShort(_ schema: Schema) throws -> Int16 { + throw notImplemented + } + + func readInteger(_ schema: Schema) throws -> Int { + throw notImplemented + } + + func readLong(_ schema: Schema) throws -> Int { + throw notImplemented + } + + func readFloat(_ schema: Schema) throws -> Float { + throw notImplemented + } + + func readDouble(_ schema: Schema) throws -> Double { + throw notImplemented + } + + func readBigInteger(_ schema: Schema) throws -> Int64 { + throw notImplemented + } + + func readBigDecimal(_ schema: Schema) throws -> Double { + throw notImplemented + } + + func readString(_ schema: Schema) throws -> String { + throw notImplemented + } + + func readDocument(_ schema: Schema) throws -> Document { + throw notImplemented + } + + func readTimestamp(_ schema: Schema) throws -> Date { + throw notImplemented + } + + func readNull(_ schema: Schema) throws -> T? { + throw notImplemented + } + + func isNull() throws -> Bool { + throw notImplemented + } + var containerSize: Int { -1 } private var notImplemented: SerializerError { .init("Not implemented") } diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index c73cf6bfb..1d6537f51 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -190,6 +190,7 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { guard var modeledError = error as? ServiceError & HTTPError & Error else { throw ClientError.invalidValue( "Modeled error does not conform to ServiceError & HTTPError & Error." + + " This should never happen, please file a bug on aws-sdk-swift." ) } modeledError.message = baseError.message From 6f7ca6b9024fcbf015884a247a778611c4577295 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 8 May 2026 15:28:25 -0400 Subject: [PATCH 41/41] fixup: deslop + epic-pattern alignment for SBS RestXML Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Schemas/SchemasCodegen.swift | 3 +- .../EventContentDeserializer.swift | 7 +- .../Deserializers/RawBlobDeserializer.swift | 5 +- Sources/SmithyRestXML/BaseError.swift | 34 +++------ Sources/SmithyRestXML/Deserializer.swift | 34 ++------- .../SmithyRestXML/HTTPClientProtocol.swift | 75 ++++++++++++------- Sources/SmithyRestXML/Plugin.swift | 8 +- Sources/SmithyRestXML/Serializer.swift | 12 --- Sources/SmithyRestXML/TimestampUtils.swift | 2 +- Sources/SmithyXML/Reader/Reader.swift | 10 +-- Sources/SmithyXML/Writer/Writer.swift | 1 - 11 files changed, 77 insertions(+), 114 deletions(-) diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift index bb578dcc4..081671c5c 100644 --- a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -140,8 +140,7 @@ package struct SchemasCodegen { // Get all the trait IDs that apply to this member & sort let memberTraitIDs = Set(memberShape.traits.traitDict.keys) let targetTraitIDs = Set(try memberShape.target.traits.traitDict.keys) - // XmlNameTrait on a target shape changes that shape's own root element name, - // but does NOT change the member element name. Only the member's own @xmlName does. + // @xmlName on the target renames the target shape, not the member; don't inherit. let inheritableTargetTraitIDs = targetTraitIDs.filter { $0 != XmlNameTrait.id } let allTraitIDs = Array(memberTraitIDs.union(inheritableTargetTraitIDs)).smithySorted() diff --git a/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift b/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift index 94a41317a..7206e5e35 100644 --- a/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift +++ b/Sources/SmithyEventStreams/Deserializers/EventContentDeserializer.swift @@ -33,18 +33,15 @@ struct EventContentDeserializer: ShapeDeserializer { // Deserialize the event payload, to the member marked with @eventPayload if it exists, // to the structure's members otherwise. if let payloadMember = schema.members.first(where: { $0.hasTrait(EventPayloadTrait.self) }) { - // For blob @eventPayload members, return the raw bytes directly without - // parsing through the codec (which would try to parse as XML/JSON). - if payloadMember.type == .blob { + // Skip codec for blob payloads — codec would parse as XML/JSON. + if (payloadMember.target ?? payloadMember).type == .blob { let blobDeserializer = RawBlobDeserializer(data: message.payload) try T.readConsumer(payloadMember, &value, blobDeserializer) } else { - // For structured @eventPayload members, use the protocol codec. let payloadDeserializer = try codec.makeDeserializer(data: message.payload) try T.readConsumer(payloadMember, &value, payloadDeserializer) } } else { - // No @eventPayload member — deserialize the whole structure from the payload. let payloadDeserializer = try codec.makeDeserializer(data: message.payload) try payloadDeserializer.readStruct(schema, &value) } diff --git a/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift index dec9f2e5f..eab8e5da2 100644 --- a/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift +++ b/Sources/SmithyEventStreams/Deserializers/RawBlobDeserializer.swift @@ -16,10 +16,7 @@ import struct SmithySerialization.SerializerError @_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeDeserializer -/// A minimal deserializer that returns raw bytes for blob reads. -/// -/// Used for `@eventPayload` members of blob type, where the event payload -/// should be returned as-is without protocol-specific parsing (e.g. XML). +/// Returns raw bytes for blob reads; used for blob @eventPayload members. struct RawBlobDeserializer: ShapeDeserializer { let data: Data diff --git a/Sources/SmithyRestXML/BaseError.swift b/Sources/SmithyRestXML/BaseError.swift index 803449ce2..6f55c58cb 100644 --- a/Sources/SmithyRestXML/BaseError.swift +++ b/Sources/SmithyRestXML/BaseError.swift @@ -15,64 +15,50 @@ import typealias SmithySerialization.ReadStructConsumer @_spi(SchemaBasedSerde) import protocol SmithySerialization.ShapeDeserializer -@_spi(RestXML) -@_spi(SchemaBasedSerde) -public struct BaseError { - public var code: String? - public var message: String? - public var requestId: String? +struct BaseError { + var code: String? + var message: String? } extension BaseError: DeserializableStruct { - private static var errorSchema: Smithy.Schema { + private static var schema: Smithy.Schema { .init( - id: .init("swift.synthetic", "RestXMLBaseError"), + id: .init("swift.synthetic", "BaseError"), type: .structure, members: [ .init( - id: .init("swift.synthetic", "RestXMLBaseError", "Code"), + id: .init("swift.synthetic", "BaseError", "Code"), type: .member, - containerType: .structure, target: Prelude.stringSchema, index: 0 ), .init( - id: .init("swift.synthetic", "RestXMLBaseError", "Message"), + id: .init("swift.synthetic", "BaseError", "Message"), type: .member, - containerType: .structure, target: Prelude.stringSchema, index: 1 ), - .init( - id: .init("swift.synthetic", "RestXMLBaseError", "RequestId"), - type: .member, - containerType: .structure, - target: Prelude.stringSchema, - index: 2 - ), ] ) } - public static var readConsumer: SmithySerialization.ReadStructConsumer { + static var readConsumer: SmithySerialization.ReadStructConsumer { { memberSchema, value, deserializer in switch memberSchema.index { case 0: value.code = try deserializer.readString(memberSchema) case 1: value.message = try deserializer.readString(memberSchema) - case 2: - value.requestId = try deserializer.readString(memberSchema) default: break } } } - public static func deserialize(_ deserializer: any ShapeDeserializer) throws -> Self { + static func deserialize(_ deserializer: any ShapeDeserializer) throws -> Self { var value = Self() - try deserializer.readStruct(Self.errorSchema, &value) + try deserializer.readStruct(Self.schema, &value) return value } } diff --git a/Sources/SmithyRestXML/Deserializer.swift b/Sources/SmithyRestXML/Deserializer.swift index 778a0cbfa..7f604ea3d 100644 --- a/Sources/SmithyRestXML/Deserializer.swift +++ b/Sources/SmithyRestXML/Deserializer.swift @@ -49,12 +49,9 @@ public struct Deserializer: ShapeDeserializer { let httpResponse: HTTPResponse? let rawBodyData: Data? let bodyStream: ByteStream? - /// Set to true when this Deserializer's Reader holds a list of values synthesized from an - /// HTTP header (each Reader child wraps one comma-split element). In that case, `readList` - /// should enumerate `reader.children` directly without filtering by XML element name. + /// Reader holds comma-split header list children rather than XML element children. let isHeaderList: Bool - /// Set to true when this Deserializer's Reader holds a value from an HTTP header. - /// Changes the default timestamp format from .dateTime (XML) to .httpDate (Smithy HTTP spec). + /// Switches the default timestamp format from `.dateTime` (XML) to `.httpDate` (HTTP spec). let isFromHttpHeader: Bool public init(data: Data) throws { @@ -95,8 +92,6 @@ public struct Deserializer: ShapeDeserializer { if bodyData.isEmpty { self.reader = Reader() } else { - // Non-XML bodies (raw blob/string @httpPayload) would fail to parse here; - // in that case we still need a Reader, so fall back to empty. self.reader = (try? Reader.from(data: bodyData)) ?? Reader() } } @@ -132,9 +127,7 @@ public struct Deserializer: ShapeDeserializer { if let memberDeserializer = try httpBindingDeserializer(for: member) { do { try T.readConsumer(member, &value, memberDeserializer) - } catch is DecodedNull { - // skip null - } + } catch is DecodedNull {} continue } let elementName = xmlElementName(for: member) @@ -143,7 +136,7 @@ public struct Deserializer: ShapeDeserializer { if isAttribute { childReader = reader[NodeInfo(elementName, location: .attribute)] } else if reader.nodeInfo.name == elementName && reader.hasContent && reader.children.isEmpty { - // Unwrapped output: the reader itself IS the member value (e.g. @s3UnwrappedXmlOutput leaf) + // @s3UnwrappedXmlOutput: leaf reader is itself the member value. childReader = reader } else { childReader = reader[NodeInfo(elementName)] @@ -152,21 +145,16 @@ public struct Deserializer: ShapeDeserializer { do { let memberDeserializer = Deserializer(reader: childReader) try T.readConsumer(member, &value, memberDeserializer) - } catch is DecodedNull { - // skip null - } + } catch is DecodedNull {} } } - /// If `member` has an HTTP binding trait (httpHeader, httpPrefixHeaders, httpResponseCode, httpPayload) - /// returns a Deserializer backed by the appropriate part of the HTTP response rather than the XML body. - /// Returns nil for normal body-bound members (the XML path should be used). + /// Returns a Deserializer for HTTP-bound members; nil for body-bound. private func httpBindingDeserializer(for member: Schema) throws -> Deserializer? { guard let httpResponse else { return nil } if let headerTrait = try member.getTrait(HttpHeaderTrait.self) { guard let headerValue = httpResponse.headers.value(for: headerTrait.value) else { return nil } - // For list-typed headers, split comma-separated values into children. if member.target?.type == .list || member.type == .list { let memberTarget = member.target?.member.target ?? member.target?.member let isTimestamp = memberTarget?.type == .timestamp @@ -201,19 +189,14 @@ public struct Deserializer: ShapeDeserializer { let targetType = member.target?.type ?? member.type switch targetType { case .structure, .union: - // Structure payload: the entire body is the payload struct. Use the root reader. - // If the body is empty, return nil so the member stays nil. guard reader.hasContent || !reader.children.isEmpty else { return nil } return Deserializer(reader: reader, httpResponse: httpResponse, rawBodyData: rawBodyData) case .blob: - // Streaming blob payload: pass the body stream through. if let bodyStream { return Deserializer(reader: Reader(), httpResponse: httpResponse, bodyStream: bodyStream) } - // Only return a deserializer if there's actual body data. guard let rawBodyData, !rawBodyData.isEmpty else { return nil } - // Reader.readIfPresent() for Data expects base64. For a raw blob payload we need - // the bytes as-is; stash them so readBlob can return them directly. + // Reader.readIfPresent(Data) decodes base64 — for a raw blob, stash and return as-is. return Deserializer(reader: Reader(), httpResponse: httpResponse, rawBodyData: rawBodyData) case .string, .enum: guard let rawBodyData, !rawBodyData.isEmpty, @@ -237,9 +220,6 @@ public struct Deserializer: ShapeDeserializer { if ch == "\"" { inQuotes.toggle(); current.append(ch); continue } if ch == "," && !inQuotes { let trimmed = current.trimmingCharacters(in: .whitespaces) - // HTTP dates have the form "DDD, DD MMM YYYY HH:MM:SS GMT". - // The day-of-week abbreviation (3 letters) is followed by a comma. - // Detect this: if the accumulated part is exactly 3 alpha chars, it's a partial date. if isTimestamp && trimmed.count == 3 && trimmed.allSatisfy(\.isLetter) { current.append(ch) continue diff --git a/Sources/SmithyRestXML/HTTPClientProtocol.swift b/Sources/SmithyRestXML/HTTPClientProtocol.swift index 1d6537f51..dfca66296 100644 --- a/Sources/SmithyRestXML/HTTPClientProtocol.swift +++ b/Sources/SmithyRestXML/HTTPClientProtocol.swift @@ -48,32 +48,59 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { public let id = ShapeID("aws.protocols", "restXml") public let codec: SmithySerialization.Codec = Codec() public let noErrorWrapping: Bool + public let handleEmpty404: Bool - /// Optional hook called for non-2xx responses before the generic error path. - /// If it returns an error, that error is thrown. If it returns nil, the generic - /// TypeRegistry-based error matching runs as normal. - /// Signature: (HTTPResponse, Data, TypeRegistry, Bool) async throws -> Error? - public let customErrorResolver: ( + private let customErrorResolver: ( @Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)? )? - /// Optional hook called after a modeled error is deserialized from the TypeRegistry. - /// Allows setting additional properties (e.g. requestID2) on the error before it is thrown. - /// Signature: (inout ServiceError & HTTPError & Error, HTTPResponse) -> Void - public let errorPostProcessor: ( + private let errorPostProcessor: ( @Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void )? - public init( - noErrorWrapping: Bool = false, - customErrorResolver: (@Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)?)? = nil, - errorPostProcessor: (@Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void)? = nil + public init(noErrorWrapping: Bool = false, handleEmpty404: Bool = false) { + self.init( + noErrorWrapping: noErrorWrapping, + handleEmpty404: handleEmpty404, + customErrorResolver: nil, + errorPostProcessor: nil + ) + } + + private init( + noErrorWrapping: Bool, + handleEmpty404: Bool, + customErrorResolver: (@Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)?)?, + errorPostProcessor: (@Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void)? ) { self.noErrorWrapping = noErrorWrapping + self.handleEmpty404 = handleEmpty404 self.customErrorResolver = customErrorResolver self.errorPostProcessor = errorPostProcessor } + public func withCustomErrorResolver( + _ resolver: @escaping @Sendable (HTTPResponse, Data, TypeRegistry, Bool) async throws -> (any Error)? + ) -> HTTPClientProtocol { + HTTPClientProtocol( + noErrorWrapping: noErrorWrapping, + handleEmpty404: handleEmpty404, + customErrorResolver: resolver, + errorPostProcessor: errorPostProcessor + ) + } + + public func withErrorPostProcessor( + _ postProcessor: @escaping @Sendable (inout any (ServiceError & HTTPError & Error), HTTPResponse) -> Void + ) -> HTTPClientProtocol { + HTTPClientProtocol( + noErrorWrapping: noErrorWrapping, + handleEmpty404: handleEmpty404, + customErrorResolver: customErrorResolver, + errorPostProcessor: postProcessor + ) + } + public func serializeRequest( operation: Operation, input: Input, @@ -102,8 +129,6 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { response: HTTPResponse ) async throws -> Output { if (200..<300).contains(response.statusCode.rawValue) { - // Check if the output has a streaming event stream @httpPayload member - // (e.g. S3 SelectObjectContent). If so, use EventStreamDeserializer. let hasEventStreamPayload = operation.outputSchema.members.contains { member in guard member.hasTrait(HttpPayloadTrait.self) else { return false } guard member.hasTrait(StreamingTrait.self) @@ -115,10 +140,6 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { let eventStreamDeserializer = EventStreamDeserializer(codec: codec, response: response) return try Output.deserialize(eventStreamDeserializer) } - // Check if the output has a streaming @httpPayload member (e.g. S3 GetObject body). - // If so, pass the ByteStream through without consuming it. - // Check both the member's own traits (which inherit from the target) and the - // target's traits directly, to be resilient to trait resolution differences. let hasStreamingPayload = operation.outputSchema.members.contains { member in guard member.hasTrait(HttpPayloadTrait.self) else { return false } return member.hasTrait(StreamingTrait.self) @@ -135,7 +156,6 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { let bodyData = try await response.body.readData() ?? Data() let errorTypeRegistry = operation.errorTypeRegistry - // If a custom error resolver is set, try it first. if let customErrorResolver, let resolvedError = try await customErrorResolver( response, bodyData, errorTypeRegistry, noErrorWrapping @@ -143,8 +163,15 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { throw resolvedError } - // Parse error response; RestXML errors may be wrapped in element - let errorDeserializer = try Deserializer(data: bodyData) + // Tolerate non-XML error bodies (HTML 5xx pages, truncated responses) + // by surfacing UnknownHTTPServiceError instead of leaking a parse error. + guard let errorDeserializer = try? Deserializer(data: bodyData) else { + throw UnknownHTTPServiceError( + httpResponse: response, + message: nil, + typeName: nil + ) + } let errorReader = errorDeserializer.reader let baseErrorDeserializer: Deserializer @@ -165,9 +192,7 @@ public struct HTTPClientProtocol: SmithySerialization.ClientProtocol, Sendable { registryEntry = errorTypeRegistry.find { entry in entry.schema.id.name == code } - } else if bodyData.isEmpty && response.statusCode == .notFound { - // S3 customization: HEAD on nonexistent object returns 404 with empty body. - // Match NotFound error by name when body is empty and status is 404. + } else if handleEmpty404 && bodyData.isEmpty && response.statusCode == .notFound { registryEntry = errorTypeRegistry.find { entry in entry.schema.id.name == "NotFound" } diff --git a/Sources/SmithyRestXML/Plugin.swift b/Sources/SmithyRestXML/Plugin.swift index 198249667..e38b144e4 100644 --- a/Sources/SmithyRestXML/Plugin.swift +++ b/Sources/SmithyRestXML/Plugin.swift @@ -6,13 +6,9 @@ // import ClientRuntime -import class SmithyHTTPAPI.HTTPRequest -import class SmithyHTTPAPI.HTTPResponse -@_spi(SchemaBasedSerde) -import protocol SmithySerialization.DeserializableStruct -@_spi(SchemaBasedSerde) -import protocol SmithySerialization.SerializableStruct +// RestXML URL paths come from per-operation @http traits, so no protocol-level +// interceptor wiring is needed here. @_spi(SchemaBasedSerde) public struct Plugin: ClientRuntime.Plugin { diff --git a/Sources/SmithyRestXML/Serializer.swift b/Sources/SmithyRestXML/Serializer.swift index f67395fef..0df02ed50 100644 --- a/Sources/SmithyRestXML/Serializer.swift +++ b/Sources/SmithyRestXML/Serializer.swift @@ -58,7 +58,6 @@ public final class Serializer: ShapeSerializer { public init() {} public func writeStruct(_ schema: Schema, _ value: S) throws { - // If any member has @httpPayload, serialize only that member as the root element. if let payloadMember = schema.members.first(where: { $0.hasTrait(HttpPayloadTrait.self) }) { let payloadSerializer = PayloadMemberSerializer(parent: self) try S.writeConsumer(payloadMember, value, payloadSerializer) @@ -106,8 +105,6 @@ public final class Serializer: ShapeSerializer { } } -/// Writes struct members as child elements of a parent Writer node. -/// This is the workhorse serializer — all member writes flow through here. private final class MemberSerializer: ShapeSerializer { let parent: Writer @@ -223,7 +220,6 @@ private final class MemberSerializer: ShapeSerializer { var data: Data { get throws { Data() } } } -/// Writes a value directly into a Writer node (used for list/map element consumers). private struct ValueSerializer: ShapeSerializer { let writer: Writer @@ -287,7 +283,6 @@ private func xmlNodeInfo(for schema: Schema) -> NodeInfo { return NodeInfo(name, location: location, namespaceDef: namespaceDef) } -/// Returns true if the member schema is bound to an HTTP location other than the body. private func isHttpBound(_ schema: Schema) -> Bool { schema.hasTrait(HttpHeaderTrait.self) || schema.hasTrait(HttpLabelTrait.self) || @@ -297,9 +292,6 @@ private func isHttpBound(_ schema: Schema) -> Bool { schema.hasTrait(HttpResponseCodeTrait.self) } -/// Serializes an @httpPayload member as the root of the document. -/// For structure/union payloads, the payload value becomes the root XML element. -/// For blob/string payloads, the raw bytes are written directly. private final class PayloadMemberSerializer: ShapeSerializer { let outer: Serializer @@ -308,7 +300,6 @@ private final class PayloadMemberSerializer: ShapeSerializer { } func writeStruct(_ schema: Schema, _ value: S) throws { - // Use member's own @xmlName if present; otherwise use the target shape's name/xmlName let nodeInfo = schema.hasTrait(XmlNameTrait.self) ? xmlNodeInfo(for: schema) : xmlNodeInfo(for: schema.target ?? schema) @@ -322,17 +313,14 @@ private final class PayloadMemberSerializer: ShapeSerializer { } func writeBlob(_ schema: Schema, _ value: Data) throws { - // Raw blob payload — store as-is (not base64-encoded XML) outer.rawBlobData = value } func writeDataStream(_ schema: Schema, _ value: ByteStream) throws { - // Streaming blob payload — pass through as-is outer.streamingBody = value } func writeString(_ schema: Schema, _ value: String) throws { - // String payload — store as UTF-8 bytes outer.rawBlobData = Data(value.utf8) } diff --git a/Sources/SmithyRestXML/TimestampUtils.swift b/Sources/SmithyRestXML/TimestampUtils.swift index 3dca8f7b5..1620b73b2 100644 --- a/Sources/SmithyRestXML/TimestampUtils.swift +++ b/Sources/SmithyRestXML/TimestampUtils.swift @@ -13,7 +13,7 @@ import struct Smithy.TimestampFormatTrait func resolveTimestampFormat(_ schema: Schema) -> TimestampFormat { guard let traitFormat = try? schema.getTrait(TimestampFormatTrait.self)?.format else { - return .dateTime // XML default + return .dateTime } switch traitFormat { case .dateTime: return .dateTime diff --git a/Sources/SmithyXML/Reader/Reader.swift b/Sources/SmithyXML/Reader/Reader.swift index 9acbcfe3c..bd2d335ab 100644 --- a/Sources/SmithyXML/Reader/Reader.swift +++ b/Sources/SmithyXML/Reader/Reader.swift @@ -23,22 +23,18 @@ public final class Reader: SmithyReader { public var hasContent: Bool { content != nil } var content: String? - // MARK: - init & deinit - - /// Creates an "empty" reader. This Reader will be returned when the data cannot be parsed. + /// An empty reader, returned when data cannot be parsed. public init() { self.nodeInfo = "" } - /// Creates a Reader backed by a literal string content. Used to adapt non-XML sources - /// (e.g. HTTP header values) into the Reader API so existing read methods work uniformly. + /// A Reader holding only literal text content (no element name). public init(content: String) { self.nodeInfo = "" self.content = content } - /// Creates a Reader with a given nodeInfo and text content. Used to synthesize - /// Reader trees matching the shape that `readMap` / `readList` expect. + /// A Reader with a node name and optional text content. public init(nodeInfo: NodeInfo, content: String?) { self.nodeInfo = nodeInfo self.content = content diff --git a/Sources/SmithyXML/Writer/Writer.swift b/Sources/SmithyXML/Writer/Writer.swift index 3d3d748d8..2a69e7d2f 100644 --- a/Sources/SmithyXML/Writer/Writer.swift +++ b/Sources/SmithyXML/Writer/Writer.swift @@ -29,7 +29,6 @@ public final class Writer: SmithyWriter { let nodeInfo: NodeInfo public var isCollection = false public var nodeInfoPath: [NodeInfo] { (parent?.nodeInfoPath ?? []) + [nodeInfo] } - // MARK: - init & deinit /// Used by the `DocumentWriter` to begin serialization of a model to XML. /// - Parameter nodeInfo: The node info for the XML node.