From 2caff830b3c7fb6e1cd3a235d955d52e189e2085 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 10 Jul 2023 12:24:03 +0100 Subject: [PATCH 01/10] Rename HTTPBody to AWSHTTPBody --- Sources/SotoCore/AWSClient.swift | 2 +- Sources/SotoCore/AWSService.swift | 2 +- Sources/SotoCore/Doc/AWSShape.swift | 8 +++---- .../SotoCore/Encoder/DictionaryDecoder.swift | 2 +- Sources/SotoCore/HTTP/AWSHTTPTypes.swift | 18 +++++++-------- Sources/SotoCore/Message/AWSRequest.swift | 14 ++++++------ Sources/SotoCore/Message/AWSResponse.swift | 2 +- .../Middleware/LoggingMiddleware.swift | 2 +- Tests/SotoCoreTests/AWSClientTests.swift | 22 +++++++++---------- Tests/SotoCoreTests/AWSRequestTests.swift | 8 +++---- Tests/SotoCoreTests/AWSResponseTests.swift | 4 ++-- Tests/SotoCoreTests/PayloadTests.swift | 6 ++--- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Sources/SotoCore/AWSClient.swift b/Sources/SotoCore/AWSClient.swift index 399485cc7..7d2461a72 100644 --- a/Sources/SotoCore/AWSClient.swift +++ b/Sources/SotoCore/AWSClient.swift @@ -521,7 +521,7 @@ extension AWSClient { url: URL, httpMethod: HTTPMethod, headers: HTTPHeaders = HTTPHeaders(), - body: HTTPBody, + body: AWSHTTPBody, serviceConfig: AWSServiceConfig, logger: Logger = AWSClient.loggingDisabled ) async throws -> HTTPHeaders { diff --git a/Sources/SotoCore/AWSService.swift b/Sources/SotoCore/AWSService.swift index 01c71975e..c93ad16a7 100644 --- a/Sources/SotoCore/AWSService.swift +++ b/Sources/SotoCore/AWSService.swift @@ -98,7 +98,7 @@ extension AWSService { url: URL, httpMethod: HTTPMethod, headers: HTTPHeaders = HTTPHeaders(), - body: HTTPBody = .init(), + body: AWSHTTPBody = .init(), logger: Logger = AWSClient.loggingDisabled ) async throws -> HTTPHeaders { return try await self.client.signHeaders(url: url, httpMethod: httpMethod, headers: headers, body: body, serviceConfig: self.config, logger: logger) diff --git a/Sources/SotoCore/Doc/AWSShape.swift b/Sources/SotoCore/Doc/AWSShape.swift index 5d10b2ea1..99892bced 100644 --- a/Sources/SotoCore/Doc/AWSShape.swift +++ b/Sources/SotoCore/Doc/AWSShape.swift @@ -141,7 +141,7 @@ public extension AWSEncodableShape { guard value.base64count <= max else { throw Self.validationError("Length of \(parent).\(name) (\(value.base64count)) is greater than the maximum allowed value \(max).") } } - func validate(_ value: HTTPBody, name: String, parent: String, min: Int) throws { + func validate(_ value: AWSHTTPBody, name: String, parent: String, min: Int) throws { if let size = value.length { guard size >= min else { throw Self.validationError("Length of \(parent).\(name) (\(size)) is less than minimum allowed value \(min).") @@ -149,7 +149,7 @@ public extension AWSEncodableShape { } } - func validate(_ value: HTTPBody, name: String, parent: String, max: Int) throws { + func validate(_ value: AWSHTTPBody, name: String, parent: String, max: Int) throws { if let size = value.length { guard size <= max else { throw Self.validationError("Length of \(parent).\(name) (\(size)) is greater than the maximum allowed value \(max).") @@ -205,12 +205,12 @@ public extension AWSEncodableShape { try validate(value, name: name, parent: parent, max: max) } - func validate(_ value: HTTPBody?, name: String, parent: String, min: Int) throws { + func validate(_ value: AWSHTTPBody?, name: String, parent: String, min: Int) throws { guard let value = value else { return } try validate(value, name: name, parent: parent, min: min) } - func validate(_ value: HTTPBody?, name: String, parent: String, max: Int) throws { + func validate(_ value: AWSHTTPBody?, name: String, parent: String, max: Int) throws { guard let value = value else { return } try validate(value, name: name, parent: parent, max: max) } diff --git a/Sources/SotoCore/Encoder/DictionaryDecoder.swift b/Sources/SotoCore/Encoder/DictionaryDecoder.swift index d4ea59e9d..030461f3c 100644 --- a/Sources/SotoCore/Encoder/DictionaryDecoder.swift +++ b/Sources/SotoCore/Encoder/DictionaryDecoder.swift @@ -1362,7 +1362,7 @@ extension __DictionaryDecoder { return try self.unbox(value, as: Data.self) } else if type == Date.self { return try self.unbox(value, as: Date.self) - } else if type == HTTPBody.self { + } else if type == AWSHTTPBody.self { return value } else { self.storage.push(container: value) diff --git a/Sources/SotoCore/HTTP/AWSHTTPTypes.swift b/Sources/SotoCore/HTTP/AWSHTTPTypes.swift index ea323bfeb..0f4035a70 100644 --- a/Sources/SotoCore/HTTP/AWSHTTPTypes.swift +++ b/Sources/SotoCore/HTTP/AWSHTTPTypes.swift @@ -19,7 +19,7 @@ import NIOHTTP1 /// Storage for HTTP body which can be either a ByteBuffer or an AsyncSequence of /// ByteBuffers -public struct HTTPBody: Sendable { +public struct AWSHTTPBody: Sendable { enum Storage { case byteBuffer(ByteBuffer) case asyncSequence(sequence: AnyAsyncSequence, length: Int?) @@ -79,7 +79,7 @@ public struct HTTPBody: Sendable { } } -extension HTTPBody: AsyncSequence { +extension AWSHTTPBody: AsyncSequence { public typealias Element = ByteBuffer public typealias AsyncIterator = AnyAsyncSequence.AsyncIterator @@ -93,11 +93,11 @@ extension HTTPBody: AsyncSequence { } } -extension HTTPBody: Decodable { - // HTTPBody has to conform to Decodable so I can add it to AWSShape objects (which conform to Decodable). But we don't want the +extension AWSHTTPBody: Decodable { + // AWSHTTPBody has to conform to Decodable so I can add it to AWSShape objects (which conform to Decodable). But we don't want the // Encoder/Decoder ever to process a AWSPayload public init(from decoder: Decoder) throws { - preconditionFailure("Cannot decode an HTTPBody") + preconditionFailure("Cannot decode an AWSHTTPBody") } } @@ -106,9 +106,9 @@ struct AWSHTTPRequest { let url: URL let method: HTTPMethod let headers: HTTPHeaders - let body: HTTPBody + let body: AWSHTTPBody - init(url: URL, method: HTTPMethod, headers: HTTPHeaders = [:], body: HTTPBody = .init()) { + init(url: URL, method: HTTPMethod, headers: HTTPHeaders = [:], body: AWSHTTPBody = .init()) { self.url = url self.method = method self.headers = headers @@ -119,7 +119,7 @@ struct AWSHTTPRequest { /// Generic HTTP Response returned from HTTP Client struct AWSHTTPResponse: Sendable { /// Initialize AWSHTTPResponse - init(status: HTTPResponseStatus, headers: HTTPHeaders, body: HTTPBody = .init()) { + init(status: HTTPResponseStatus, headers: HTTPHeaders, body: AWSHTTPBody = .init()) { self.status = status self.headers = headers self.body = body @@ -132,5 +132,5 @@ struct AWSHTTPResponse: Sendable { var headers: HTTPHeaders /// The body of this HTTP response. - var body: HTTPBody + var body: AWSHTTPBody } diff --git a/Sources/SotoCore/Message/AWSRequest.swift b/Sources/SotoCore/Message/AWSRequest.swift index 8f4dc9f55..cb433e059 100644 --- a/Sources/SotoCore/Message/AWSRequest.swift +++ b/Sources/SotoCore/Message/AWSRequest.swift @@ -37,7 +37,7 @@ public struct AWSRequest { /// request headers public var httpHeaders: HTTPHeaders /// request body - public var body: HTTPBody + public var body: AWSHTTPBody /// Create HTTP Client request from AWSRequest. /// If the signer's credentials are available the request will be signed. Otherwise defaults to an unsigned request @@ -74,7 +74,7 @@ public struct AWSRequest { // create s3 signed Sequence let s3Signed = sequence.s3Signed(signer: signer, seedSigningData: seedSigningData) // create new payload and return request - let payload = HTTPBody(asyncSequence: s3Signed, length: s3Signed.contentSize(from: length!)) + let payload = AWSHTTPBody(asyncSequence: s3Signed, length: s3Signed.contentSize(from: length!)) return AWSHTTPRequest(url: url, method: httpMethod, headers: signedHeaders, body: payload) } else { bodyDataForSigning = .unsignedPayload @@ -201,14 +201,14 @@ extension AWSRequest { } var raw = false - let body: HTTPBody + let body: AWSHTTPBody switch configuration.serviceProtocol { case .json, .restjson: if let shapeWithPayload = Input.self as? AWSShapeWithPayload.Type { let payload = shapeWithPayload._payloadPath if let payloadBody = mirror.getAttribute(forKey: payload) { switch payloadBody { - case let awsPayload as HTTPBody: + case let awsPayload as AWSHTTPBody: Self.verifyStream(operation: operationName, payload: awsPayload, input: shapeWithPayload) body = awsPayload raw = true @@ -237,7 +237,7 @@ extension AWSRequest { let payload = shapeWithPayload._payloadPath if let payloadBody = mirror.getAttribute(forKey: payload) { switch payloadBody { - case let awsPayload as HTTPBody: + case let awsPayload as AWSHTTPBody: Self.verifyStream(operation: operationName, payload: awsPayload, input: shapeWithPayload) body = awsPayload case let shape as AWSEncodableShape: @@ -341,7 +341,7 @@ extension AWSRequest { /// - Returns: New set of headers private static func calculateChecksumHeader( headers: HTTPHeaders, - body: HTTPBody, + body: AWSHTTPBody, shapeType: Input.Type, configuration: AWSServiceConfig ) -> HTTPHeaders { @@ -434,7 +434,7 @@ extension AWSRequest { } /// verify streaming is allowed for this operation - internal static func verifyStream(operation: String, payload: HTTPBody, input: AWSShapeWithPayload.Type) { + internal static func verifyStream(operation: String, payload: AWSHTTPBody, input: AWSShapeWithPayload.Type) { guard case .asyncSequence(_, let length) = payload.storage else { return } precondition(input._options.contains(.allowStreaming), "\(operation) does not allow streaming of data") precondition(length != nil || input._options.contains(.allowChunkedStreaming), "\(operation) does not allow chunked streaming of data. Please supply a data size.") diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index 69955fac4..f1e09aaa4 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -28,7 +28,7 @@ public struct AWSResponse { /// response headers public var headers: HTTPHeaders /// response body - public var body: HTTPBody + public var body: AWSHTTPBody /// initialize an AWSResponse Object /// - parameters: diff --git a/Sources/SotoCore/Message/Middleware/LoggingMiddleware.swift b/Sources/SotoCore/Message/Middleware/LoggingMiddleware.swift index ee615798f..6fd0f431b 100644 --- a/Sources/SotoCore/Message/Middleware/LoggingMiddleware.swift +++ b/Sources/SotoCore/Message/Middleware/LoggingMiddleware.swift @@ -33,7 +33,7 @@ public struct AWSLoggingMiddleware: AWSServiceMiddleware { self.log = { logger.log(level: logLevel, "\($0())") } } - func getBodyOutput(_ body: HTTPBody) -> String { + func getBodyOutput(_ body: AWSHTTPBody) -> String { var output = "" switch body.storage { case .byteBuffer(let buffer): diff --git a/Tests/SotoCoreTests/AWSClientTests.swift b/Tests/SotoCoreTests/AWSClientTests.swift index 8f65a0244..5899023d6 100644 --- a/Tests/SotoCoreTests/AWSClientTests.swift +++ b/Tests/SotoCoreTests/AWSClientTests.swift @@ -232,14 +232,14 @@ class AWSClientTests: XCTestCase { struct Input: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath: String = "payload" static var _options: AWSShapeOptions = [.allowStreaming, .rawPayload] - let payload: HTTPBody + let payload: AWSHTTPBody private enum CodingKeys: CodingKey {} } let data = createRandomBuffer(45, 9182, size: bufferSize) var byteBuffer = ByteBufferAllocator().buffer(capacity: data.count) byteBuffer.writeBytes(data) - let payload = HTTPBody(asyncSequence: byteBuffer.asyncSequence(chunkSize: blockSize), length: bufferSize) + let payload = AWSHTTPBody(asyncSequence: byteBuffer.asyncSequence(chunkSize: blockSize), length: bufferSize) let input = Input(payload: payload) async let responseTask: Void = client.execute(operation: "test", path: "/", httpMethod: .POST, serviceConfig: config, input: input, logger: TestEnvironment.logger) @@ -291,11 +291,11 @@ class AWSClientTests: XCTestCase { try await self.testRequestStreaming(config: config, client: client, server: awsServer, bufferSize: 65552, blockSize: 65552) } - func testRequestStreamingWithPayload(_ payload: HTTPBody) async throws { + func testRequestStreamingWithPayload(_ payload: AWSHTTPBody) async throws { struct Input: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath: String = "payload" static var _options: AWSShapeOptions = [.allowStreaming] - let payload: HTTPBody + let payload: AWSHTTPBody private enum CodingKeys: CodingKey {} } @@ -317,7 +317,7 @@ class AWSClientTests: XCTestCase { func testRequestStreamingTooMuchData() async throws { // set up stream of 8 bytes but supply more than that let buffer = ByteBuffer(string: "String longer than 8 bytes") - let payload = HTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 1024), length: buffer.readableBytes - 1) + let payload = AWSHTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 1024), length: buffer.readableBytes - 1) do { try await self.testRequestStreamingWithPayload(payload) XCTFail("Should not get here") @@ -329,7 +329,7 @@ class AWSClientTests: XCTestCase { func testRequestStreamingNotEnoughData() async throws { // set up stream of 8 bytes but supply more than that let buffer = ByteBuffer(string: "String longer than 8 bytes") - let payload = HTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 1024), length: buffer.readableBytes + 1) + let payload = AWSHTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 1024), length: buffer.readableBytes + 1) do { try await self.testRequestStreamingWithPayload(payload) XCTFail("Should not get here") @@ -342,7 +342,7 @@ class AWSClientTests: XCTestCase { struct Input: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath: String = "payload" static var _options: AWSShapeOptions = [.allowStreaming, .allowChunkedStreaming, .rawPayload] - let payload: HTTPBody + let payload: AWSHTTPBody private enum CodingKeys: CodingKey {} } @@ -363,7 +363,7 @@ class AWSClientTests: XCTestCase { var byteBuffer = ByteBufferAllocator().buffer(capacity: bufferSize) byteBuffer.writeBytes(data) - let payload = HTTPBody(asyncSequence: byteBuffer.asyncSequence(chunkSize: blockSize), length: nil) + let payload = AWSHTTPBody(asyncSequence: byteBuffer.asyncSequence(chunkSize: blockSize), length: nil) let input = Input(payload: payload) async let responseTask: Void = client.execute(operation: "test", path: "/", httpMethod: .POST, serviceConfig: config, input: input, logger: TestEnvironment.logger) @@ -587,7 +587,7 @@ class AWSClientTests: XCTestCase { struct Input: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath: String = "payload" static var _options: AWSShapeOptions = [.allowStreaming, .allowChunkedStreaming, .rawPayload] - let payload: HTTPBody + let payload: AWSHTTPBody private enum CodingKeys: CodingKey {} } let retryPolicy = TestRetryPolicy() @@ -601,7 +601,7 @@ class AWSClientTests: XCTestCase { XCTAssertNoThrow(try client.syncShutdown()) XCTAssertNoThrow(try httpClient.syncShutdown()) } - let payload = HTTPBody(asyncSequence: ByteBuffer().asyncSequence(chunkSize: 16), length: nil) + let payload = AWSHTTPBody(asyncSequence: ByteBuffer().asyncSequence(chunkSize: 16), length: nil) let input = Input(payload: payload) async let responseTask: Void = client.execute( operation: "test", @@ -659,7 +659,7 @@ class AWSClientTests: XCTestCase { static let _encoding = [AWSMemberEncoding(label: "test", location: .header("test"))] static let _payloadPath: String = "payload" static let _options: AWSShapeOptions = .rawPayload - let payload: HTTPBody + let payload: AWSHTTPBody let test: String } let data = createRandomBuffer(45, 109, size: 128 * 1024) diff --git a/Tests/SotoCoreTests/AWSRequestTests.swift b/Tests/SotoCoreTests/AWSRequestTests.swift index 9ac7d4bb9..e4f269823 100644 --- a/Tests/SotoCoreTests/AWSRequestTests.swift +++ b/Tests/SotoCoreTests/AWSRequestTests.swift @@ -25,7 +25,7 @@ import SotoTestUtils import SotoXML import XCTest -extension HTTPBody { +extension AWSHTTPBody { func asString() -> String? { switch self.storage { case .byteBuffer(let buffer): @@ -170,7 +170,7 @@ class AWSRequestTests: XCTestCase { } struct Object2: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath = "payload" - let payload: HTTPBody + let payload: AWSHTTPBody private enum CodingKeys: CodingKey {} } let object = Object(string: "Name") @@ -446,7 +446,7 @@ class AWSRequestTests: XCTestCase { struct Input: AWSEncodableShape & AWSShapeWithPayload { public static let _options: AWSShapeOptions = [.rawPayload, .allowStreaming] public static let _payloadPath: String = "payload" - let payload: HTTPBody + let payload: AWSHTTPBody let member: String private enum CodingKeys: String, CodingKey { @@ -460,7 +460,7 @@ class AWSRequestTests: XCTestCase { region: config.region.rawValue ) let buffer = ByteBuffer(string: "This is a test") - let stream = HTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 16), length: buffer.readableBytes) + let stream = AWSHTTPBody(asyncSequence: buffer.asyncSequence(chunkSize: 16), length: buffer.readableBytes) let input = Input(payload: stream, member: "test") var optionalAWSRequest: AWSRequest? XCTAssertNoThrow(optionalAWSRequest = try AWSRequest(operation: "Test", path: "/", httpMethod: .POST, input: input, configuration: config)) diff --git a/Tests/SotoCoreTests/AWSResponseTests.swift b/Tests/SotoCoreTests/AWSResponseTests.swift index abbe86bc9..08d575efa 100644 --- a/Tests/SotoCoreTests/AWSResponseTests.swift +++ b/Tests/SotoCoreTests/AWSResponseTests.swift @@ -154,7 +154,7 @@ class AWSResponseTests: XCTestCase { struct Output: AWSDecodableShape, AWSShapeWithPayload { static let _payloadPath: String = "body" static let _options: AWSShapeOptions = .rawPayload - let body: HTTPBody + let body: AWSHTTPBody } let byteBuffer = ByteBuffer(string: "{\"name\":\"hello\"}") let response = AWSHTTPResponse( @@ -216,7 +216,7 @@ class AWSResponseTests: XCTestCase { public static var _encoding = [ AWSMemberEncoding(label: "contentType", location: .header("content-type")), ] - let body: HTTPBody + let body: AWSHTTPBody } let byteBuffer = ByteBuffer(string: "{\"name\":\"hello\"}") let response = AWSHTTPResponse( diff --git a/Tests/SotoCoreTests/PayloadTests.swift b/Tests/SotoCoreTests/PayloadTests.swift index 70b5f6146..cc90c4b64 100644 --- a/Tests/SotoCoreTests/PayloadTests.swift +++ b/Tests/SotoCoreTests/PayloadTests.swift @@ -18,10 +18,10 @@ import SotoTestUtils import XCTest class PayloadTests: XCTestCase { - func testRequestPayload(_ payload: HTTPBody, expectedResult: String) async { + func testRequestPayload(_ payload: AWSHTTPBody, expectedResult: String) async { struct DataPayload: AWSEncodableShape & AWSShapeWithPayload { static var _payloadPath: String = "data" - let data: HTTPBody + let data: AWSHTTPBody private enum CodingKeys: CodingKey {} } @@ -73,7 +73,7 @@ class PayloadTests: XCTestCase { struct Output: AWSDecodableShape, AWSShapeWithPayload { static let _payloadPath: String = "payload" static let _options: AWSShapeOptions = .rawPayload - let payload: HTTPBody + let payload: AWSHTTPBody } do { let awsServer = AWSTestServer(serviceProtocol: .json) From c534d122f3c71bdf96bb4151cd2147d0d50d84fd Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 26 Jun 2023 15:04:21 +0100 Subject: [PATCH 02/10] Add AWSResponse to Codable userInfo --- .../SotoCore/Concurrency/AnyAsyncSequence.swift | 2 +- Sources/SotoCore/Message/AWSResponse.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/SotoCore/Concurrency/AnyAsyncSequence.swift b/Sources/SotoCore/Concurrency/AnyAsyncSequence.swift index 72fb7145a..8b91ae7f4 100644 --- a/Sources/SotoCore/Concurrency/AnyAsyncSequence.swift +++ b/Sources/SotoCore/Concurrency/AnyAsyncSequence.swift @@ -42,7 +42,7 @@ public struct AnyAsyncSequence: Sendable, AsyncSequence { @usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback - @inlinable init( + @inlinable public init( _ asyncSequence: SequenceOfBytes ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element { self.makeAsyncIteratorCallback = { diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index f1e09aaa4..c6f4503de 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -72,6 +72,7 @@ public struct AWSResponse { } } let decoder = DictionaryDecoder() + decoder.userInfo[.awsResponse] = self var outputDict: [String: Any] = [:] switch self.body.storage { @@ -133,6 +134,8 @@ public struct AWSResponse { let node = XML.Element(name: statusCodeParam, stringValue: "\(self.status.code)") outputNode.addChild(node) } + var xmlDecoder = XMLDecoder() + xmlDecoder.userInfo[.awsResponse] = self return try XMLDecoder().decode(Output.self, from: outputNode) } } @@ -365,6 +368,14 @@ public struct AWSResponse { return nil } } + + public func headerValue(_ header: String) -> Value? where Value.RawValue == String { + self.headers[header].first.map { .init(rawValue: $0) } ?? nil + } + + public func headerValue(_ header: String) -> Value? { + self.headers[header].first.map { .init($0) } ?? nil + } } private protocol APIError { @@ -379,3 +390,7 @@ extension XML.Document { try self.init(string: xmlString) } } + +extension CodingUserInfoKey { + public static var awsResponse: Self { return .init(rawValue: "soto.awsResponse")! } +} From 1ce47acf4042e3cf1531724a906d345aebc7bba8 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 26 Jun 2023 18:41:24 +0100 Subject: [PATCH 03/10] Add decode functions for AWSResponse --- Sources/SotoCore/Message/AWSResponse.swift | 38 +++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index c6f4503de..d6485b938 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -13,6 +13,10 @@ //===----------------------------------------------------------------------===// import AsyncHTTPClient +import struct Foundation.Date +import class Foundation.DateFormatter +import struct Foundation.Locale +import struct Foundation.TimeZone import class Foundation.JSONDecoder import class Foundation.JSONSerialization import Logging @@ -368,14 +372,6 @@ public struct AWSResponse { return nil } } - - public func headerValue(_ header: String) -> Value? where Value.RawValue == String { - self.headers[header].first.map { .init(rawValue: $0) } ?? nil - } - - public func headerValue(_ header: String) -> Value? { - self.headers[header].first.map { .init($0) } ?? nil - } } private protocol APIError { @@ -394,3 +390,29 @@ extension XML.Document { extension CodingUserInfoKey { public static var awsResponse: Self { return .init(rawValue: "soto.awsResponse")! } } + +extension AWSResponse { + public func decode(_ type: Value.Type, forHeader header: String) -> Value? where Value.RawValue == String { + self.headers[header].first.map { .init(rawValue: $0) } ?? nil + } + + public func decode(_ type: Value.Type, forHeader header: String) -> Value? { + self.headers[header].first.map { .init($0) } ?? nil + } + + public func decode(_ type: Date.Type, forHeader header: String) -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + return dateFormatter.date(from: header) + } + + public func decode(_ type: [String: String].Type, forHeader header: String) -> [String: String]? { + let headers = self.headers.compactMap { $0.name.hasPrefix(header) ? $0 : nil } + if headers.count == 0 { + return nil + } + return [String: String](headers.map { (key: String($0.name.dropFirst(header.count)), value: $0.value) }) { first,_ in first } + } +} \ No newline at end of file From fb0cc52350756f1229b8b3370e7645eff9701274 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 27 Jun 2023 16:52:50 +0100 Subject: [PATCH 04/10] ResponseContainer for decoding --- .../SotoCore/Encoder/ResponseContainer.swift | 118 ++++++++++++++++++ Sources/SotoCore/Message/AWSResponse.swift | 70 +++-------- 2 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 Sources/SotoCore/Encoder/ResponseContainer.swift diff --git a/Sources/SotoCore/Encoder/ResponseContainer.swift b/Sources/SotoCore/Encoder/ResponseContainer.swift new file mode 100644 index 000000000..ca0d16a15 --- /dev/null +++ b/Sources/SotoCore/Encoder/ResponseContainer.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Soto for AWS open source project +// +// Copyright (c) 2017-2020 the Soto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Soto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import struct Foundation.Date +import class Foundation.DateFormatter +import struct Foundation.Locale +import struct Foundation.TimeZone + +public struct HeaderDecodingError: Error { + let header: String + let message: String + + static func headerNotFound(_ header: String) -> Self { .init(header: header, message: "Header not found") } + static func typeMismatch(_ header: String, expectedType: String) -> Self { .init(header: header, message: "Cannot convert header to \(expectedType)") } +} + +public struct ResponseDecodingContainer { + let response: AWSResponse + + public func decode(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value where Value.RawValue == String { + guard let headerValue = response.headers[header].first else { + throw HeaderDecodingError.headerNotFound(header) + } + if let result = Value(rawValue: headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)") + } + } + + public func decode(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value { + guard let headerValue = response.headers[header].first else { + throw HeaderDecodingError.headerNotFound(header) + } + if let result = Value(headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)") + } + } + + public func decodeStatus(_: Value.Type = Value.self) -> Value { + return Value(self.response.status.code) + } + + public func decodePayload() -> HTTPBody { + return self.response.body + } + + public func decode(_ type: Date.Type = Date.self, forHeader header: String) throws -> Date { + guard let headerValue = response.headers[header].first else { + throw HeaderDecodingError.headerNotFound(header) + } + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + if let result = dateFormatter.date(from: headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "Date") + } + } + + public func decodeIfPresent(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value? where Value.RawValue == String { + guard let headerValue = response.headers[header].first else { return nil } + if let result = Value(rawValue: headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)") + } + } + + public func decodeIfPresent(_ type: Value.Type = Value.self, forHeader header: String) throws -> Value? { + guard let headerValue = response.headers[header].first else { return nil } + if let result = Value(headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "\(Value.self)") + } + } + + public func decodeIfPresent(_ type: Date.Type = Date.self, forHeader header: String) throws -> Date? { + guard let headerValue = response.headers[header].first else { return nil } + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + if let result = dateFormatter.date(from: headerValue) { + return result + } else { + throw HeaderDecodingError.typeMismatch(header, expectedType: "Date") + } + } + + public func decodeIfPresent(_ type: [String: String].Type = [String: String].self, forHeader header: String) throws -> [String: String]? { + let headers = self.response.headers.compactMap { $0.name.hasPrefix(header) ? $0 : nil } + if headers.count == 0 { + return nil + } + return [String: String](headers.map { (key: String($0.name.dropFirst(header.count)), value: $0.value) }) { first, _ in first } + } +} + +extension CodingUserInfoKey { + public static var awsResponse: Self { return .init(rawValue: "soto.awsResponse")! } +} diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index d6485b938..80131c07b 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -15,10 +15,10 @@ import AsyncHTTPClient import struct Foundation.Date import class Foundation.DateFormatter -import struct Foundation.Locale -import struct Foundation.TimeZone import class Foundation.JSONDecoder import class Foundation.JSONSerialization +import struct Foundation.Locale +import struct Foundation.TimeZone import Logging import NIOCore import NIOFoundationCompat @@ -76,7 +76,7 @@ public struct AWSResponse { } } let decoder = DictionaryDecoder() - decoder.userInfo[.awsResponse] = self + decoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) var outputDict: [String: Any] = [:] switch self.body.storage { @@ -102,9 +102,9 @@ public struct AWSResponse { outputDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] } // if payload path is set then the decode will expect the payload to decode to the relevant member variable - if let payloadKey = payloadKey { - outputDict = [payloadKey: outputDict] - } + /* if let payloadKey = payloadKey { + outputDict = [payloadKey: outputDict] + } */ decoder.dateDecodingStrategy = .secondsSince1970 } else { // we have no body so date decoding will be only HTTP headers @@ -116,22 +116,22 @@ public struct AWSResponse { var outputNode = node // if payload path is set then the decode will expect the payload to decode to the relevant member variable. // Most CloudFront responses have this. - if let payloadKey = payloadKey { - // set output node name - outputNode.name = payloadKey - // create parent node and add output node and set output node to parent - let parentNode = XML.Element(name: "Container") - parentNode.addChild(outputNode) - outputNode = parentNode - } else if let child = node.children(of: .element)?.first as? XML.Element, - node.name == operation + "Response", - child.name == operation + "Result" + /* if let payloadKey = payloadKey { + // set output node name + outputNode.name = payloadKey + // create parent node and add output node and set output node to parent + let parentNode = XML.Element(name: "Container") + parentNode.addChild(outputNode) + outputNode = parentNode + } else */ if let child = node.children(of: .element)?.first as? XML.Element, + node.name == operation + "Response", + child.name == operation + "Result" { outputNode = child } // add headers to XML - addHeadersToXML(rootElement: outputNode, output: Output.self) + // addHeadersToXML(rootElement: outputNode, output: Output.self) // add status code to XML if let statusCodeParam = Output.statusCodeParam { @@ -139,15 +139,15 @@ public struct AWSResponse { outputNode.addChild(node) } var xmlDecoder = XMLDecoder() - xmlDecoder.userInfo[.awsResponse] = self - return try XMLDecoder().decode(Output.self, from: outputNode) + xmlDecoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) + return try xmlDecoder.decode(Output.self, from: outputNode) } } } } // add headers to output dictionary - outputDict = addHeadersToDictionary(dictionary: outputDict, output: Output.self) + // outputDict = addHeadersToDictionary(dictionary: outputDict, output: Output.self) // add status code to output dictionary if let statusCodeParam = Output.statusCodeParam { @@ -386,33 +386,3 @@ extension XML.Document { try self.init(string: xmlString) } } - -extension CodingUserInfoKey { - public static var awsResponse: Self { return .init(rawValue: "soto.awsResponse")! } -} - -extension AWSResponse { - public func decode(_ type: Value.Type, forHeader header: String) -> Value? where Value.RawValue == String { - self.headers[header].first.map { .init(rawValue: $0) } ?? nil - } - - public func decode(_ type: Value.Type, forHeader header: String) -> Value? { - self.headers[header].first.map { .init($0) } ?? nil - } - - public func decode(_ type: Date.Type, forHeader header: String) -> Date? { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - return dateFormatter.date(from: header) - } - - public func decode(_ type: [String: String].Type, forHeader header: String) -> [String: String]? { - let headers = self.headers.compactMap { $0.name.hasPrefix(header) ? $0 : nil } - if headers.count == 0 { - return nil - } - return [String: String](headers.map { (key: String($0.name.dropFirst(header.count)), value: $0.value) }) { first,_ in first } - } -} \ No newline at end of file From 39bce9f8c086cd25812676f9f91a0a63b6a5c564 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 29 Jun 2023 11:28:39 +0100 Subject: [PATCH 05/10] Fix tests after userInfo changes --- Tests/SotoCoreTests/AWSClientTests.swift | 10 +++- Tests/SotoCoreTests/AWSResponseTests.swift | 64 +++++++++++++--------- Tests/SotoCoreTests/TimeStampTests.swift | 5 +- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/Tests/SotoCoreTests/AWSClientTests.swift b/Tests/SotoCoreTests/AWSClientTests.swift index 5899023d6..5c8aec012 100644 --- a/Tests/SotoCoreTests/AWSClientTests.swift +++ b/Tests/SotoCoreTests/AWSClientTests.swift @@ -655,12 +655,16 @@ class AWSClientTests: XCTestCase { func testStreamingResponse() async { struct Input: AWSEncodableShape {} - struct Output: AWSDecodableShape & AWSShapeWithPayload { - static let _encoding = [AWSMemberEncoding(label: "test", location: .header("test"))] - static let _payloadPath: String = "payload" + struct Output: AWSDecodableShape { static let _options: AWSShapeOptions = .rawPayload let payload: AWSHTTPBody let test: String + + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.payload = response.decodePayload() + self.test = try response.decode(String.self, forHeader: "test") + } } let data = createRandomBuffer(45, 109, size: 128 * 1024) var sourceByteBuffer = ByteBufferAllocator().buffer(capacity: 128 * 1024) diff --git a/Tests/SotoCoreTests/AWSResponseTests.swift b/Tests/SotoCoreTests/AWSResponseTests.swift index 08d575efa..afd6cb86d 100644 --- a/Tests/SotoCoreTests/AWSResponseTests.swift +++ b/Tests/SotoCoreTests/AWSResponseTests.swift @@ -22,10 +22,10 @@ import XCTest class AWSResponseTests: XCTestCase { func testHeaderResponseDecoding() async throws { struct Output: AWSDecodableShape { - static let _encoding = [AWSMemberEncoding(label: "h", location: .header("header-member"))] let h: String - private enum CodingKeys: String, CodingKey { - case h = "header-member" + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.h = try response.decode(String.self, forHeader: "header-member") } } let response = AWSHTTPResponse( @@ -48,18 +48,20 @@ class AWSResponseTests: XCTestCase { func testHeaderResponseTypeDecoding() async throws { struct Output: AWSDecodableShape { - static let _encoding = [ - AWSMemberEncoding(label: "string", location: .header("string")), - AWSMemberEncoding(label: "string2", location: .header("string2")), - AWSMemberEncoding(label: "double", location: .header("double")), - AWSMemberEncoding(label: "integer", location: .header("integer")), - AWSMemberEncoding(label: "bool", location: .header("bool")), - ] let string: String let string2: String let double: Double let integer: Int let bool: Bool + + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.string = try response.decode(String.self, forHeader: "string") + self.string2 = try response.decode(String.self, forHeader: "string2") + self.double = try response.decode(Double.self, forHeader: "double") + self.integer = try response.decode(Int.self, forHeader: "integer") + self.bool = try response.decode(Bool.self, forHeader: "bool") + } } let response = AWSHTTPResponse( status: .ok, @@ -127,14 +129,14 @@ class AWSResponseTests: XCTestCase { func testValidateXMLCodablePayloadResponse() async throws { struct Output: AWSDecodableShape & AWSShapeWithPayload { - static let _encoding = [AWSMemberEncoding(label: "contentType", location: .header("content-type"))] static let _payloadPath: String = "name" let name: String let contentType: String - private enum CodingKeys: String, CodingKey { - case name - case contentType = "content-type" + init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.contentType = try response.decode(String.self, forHeader: "content-type") + self.name = try .init(from: decoder) } } let response = AWSHTTPResponse( @@ -196,6 +198,10 @@ class AWSResponseTests: XCTestCase { struct Output: AWSDecodableShape & AWSShapeWithPayload { static let _payloadPath: String = "output2" let output2: Output2 + + init(from decoder: Decoder) throws { + self.output2 = try .init(from: decoder) + } } let response = AWSHTTPResponse( status: .ok, @@ -366,9 +372,11 @@ class AWSResponseTests: XCTestCase { static let _encoding: [AWSMemberEncoding] = [ .init(label: "content", location: .headerPrefix("prefix-")), ] - let content: [String: String] - private enum CodingKeys: String, CodingKey { - case content = "prefix-" + let content: [String: String]? + + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.content = try response.decodeIfPresent([String: String].self, forHeader: "prefix-") } } let response = AWSHTTPResponse( @@ -378,20 +386,24 @@ class AWSResponseTests: XCTestCase { var output: Output? let awsResponse = try await AWSResponse(from: response, streaming: false) XCTAssertNoThrow(output = try awsResponse.generateOutputShape(operation: "Test", serviceProtocol: .restxml)) - XCTAssertEqual(output?.content["one"], "first") - XCTAssertEqual(output?.content["two"], "second") + XCTAssertEqual(output?.content?["one"], "first") + XCTAssertEqual(output?.content?["two"], "second") } func testHeaderPrefixFromXML() async throws { struct Output: AWSDecodableShape { - static let _encoding: [AWSMemberEncoding] = [ - .init(label: "content", location: .headerPrefix("prefix-")), - ] - let content: [String: String] + let content: [String: String]? let body: String + + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + let container = try decoder.container(keyedBy: CodingKeys.self) + self.content = try response.decodeIfPresent([String: String].self, forHeader: "prefix-") + self.body = try container.decode(String.self, forKey: .body) + } + private enum CodingKeys: String, CodingKey { case body - case content = "prefix-" } } let response = AWSHTTPResponse( @@ -402,8 +414,8 @@ class AWSResponseTests: XCTestCase { var output: Output? let awsResponse = try await AWSResponse(from: response, streaming: false) XCTAssertNoThrow(output = try awsResponse.generateOutputShape(operation: "Test", serviceProtocol: .restxml)) - XCTAssertEqual(output?.content["one"], "first") - XCTAssertEqual(output?.content["two"], "second") + XCTAssertEqual(output?.content?["one"], "first") + XCTAssertEqual(output?.content?["two"], "second") } // MARK: Miscellaneous tests diff --git a/Tests/SotoCoreTests/TimeStampTests.swift b/Tests/SotoCoreTests/TimeStampTests.swift index 4aadc5eb6..bdc0ccf56 100644 --- a/Tests/SotoCoreTests/TimeStampTests.swift +++ b/Tests/SotoCoreTests/TimeStampTests.swift @@ -107,8 +107,9 @@ class TimeStampTests: XCTestCase { struct A: AWSDecodableShape { static let _encoding = [AWSMemberEncoding(label: "date", location: .header("Date"))] let date: Date - private enum CodingKeys: String, CodingKey { - case date = "Date" + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.date = try response.decode(Date.self, forHeader: "Date") } } let response = AWSHTTPResponse(status: .ok, headers: ["Date": "Tue, 15 Nov 1994 12:45:27 GMT"]) From ccb3274a5fd0362bd2164ec5d278010bc934d206 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 29 Jun 2023 13:30:35 +0100 Subject: [PATCH 06/10] Remove DictionaryDecoder Now we pass the response via uesrInfo to the decoder and decode header, body and payload in the init(from) function, we can use the JSONDecoder for decoding responses. --- .../SotoCore/Encoder/DictionaryDecoder.swift | 1485 ----------------- .../SotoCore/Message/AWSResponse+HAL.swift | 14 +- Sources/SotoCore/Message/AWSResponse.swift | 161 +- .../DictionaryEncoderTests.swift | 412 ----- Tests/SotoCoreTests/JSONCoderTests.swift | 202 --- 5 files changed, 41 insertions(+), 2233 deletions(-) delete mode 100644 Sources/SotoCore/Encoder/DictionaryDecoder.swift delete mode 100644 Tests/SotoCoreTests/DictionaryEncoderTests.swift delete mode 100644 Tests/SotoCoreTests/JSONCoderTests.swift diff --git a/Sources/SotoCore/Encoder/DictionaryDecoder.swift b/Sources/SotoCore/Encoder/DictionaryDecoder.swift deleted file mode 100644 index 030461f3c..000000000 --- a/Sources/SotoCore/Encoder/DictionaryDecoder.swift +++ /dev/null @@ -1,1485 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2017-2020 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// based on JSONEncoder.swift from apple/swift as of Apr 18 2019 -// https://github.com/apple/swift/blob/2771eb520c4e3058058baf6bb3f6dba6184a17d3/stdlib/public/Darwin/Foundation/JSONEncoder.swift - -import struct Foundation.Data -import struct Foundation.Date -import class Foundation.DateFormatter -import struct Foundation.URL - -import class Foundation.NSNull -import class Foundation.NSNumber -import class Foundation.NumberFormatter - -//===----------------------------------------------------------------------===// -// Dictionary Decoders -//===----------------------------------------------------------------------===// - -/// `DictionaryDecoder` facilitates the decoding of Dictionaries into semantic `Decodable` types. -class DictionaryDecoder { - // MARK: Options - - /// The strategy to use for decoding `Date` values. - public enum DateDecodingStrategy { - /// Defer to `Date` for decoding. This is the default strategy. - case deferredToDate - - /// Decode the `Date` as a UNIX timestamp from a JSON number. - case secondsSince1970 - - /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. - case millisecondsSince1970 - - /// Decode the `Date` as a string parsed by the given formatter. - case formatted(DateFormatter) - - /// Decode the `Date` as a custom value decoded by the given closure. - case custom((_ decoder: Decoder) throws -> Date) - } - - /// The strategy to use for decoding `Data` values. - public enum DataDecodingStrategy { - /// Decode the `Data` from a Base64-encoded string. - case base64 - - /// Don't decode. This is the default strategy. - case raw - } - - /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). - public enum NonConformingFloatDecodingStrategy { - /// Throw upon encountering non-conforming values. This is the default strategy. - case `throw` - - /// Decode the values from the given representation strings. - case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) - } - - /// The strategy to use in decoding dates. Defaults to `.deferredToDate`. - public var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate - - /// The strategy to use in decoding binary data. Defaults to `.base64`. - public var dataDecodingStrategy: DataDecodingStrategy = .base64 - - /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`. - public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw - - /// Contextual user-provided information for use during decoding. - public var userInfo: [CodingUserInfoKey: Any] = [:] - - /// Options set on the top-level encoder to pass down the decoding hierarchy. - fileprivate struct _Options { - let dateDecodingStrategy: DateDecodingStrategy - let dataDecodingStrategy: DataDecodingStrategy - let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy - let userInfo: [CodingUserInfoKey: Any] - } - - /// The options set on the top-level decoder. - fileprivate var options: _Options { - return _Options( - dateDecodingStrategy: dateDecodingStrategy, - dataDecodingStrategy: dataDecodingStrategy, - nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, - userInfo: userInfo - ) - } - - // MARK: - Constructing a Dictionary Decoder - - /// Initializes `self` with default strategies. - public init() {} - - // MARK: - Decoding Values - - /// Decodes a top-level value of the given type from the given dictionary representation. - /// - /// - parameter type: The type of the value to decode. - /// - parameter from: The dictionary to decode from. - /// - returns: A value of the requested type. - /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted. - /// - throws: An error if any value throws an error during decoding. - public func decode(_ type: T.Type, from dictionary: Any) throws -> T { - let decoder = __DictionaryDecoder(referencing: dictionary, options: self.options) - guard let value = try decoder.unbox(dictionary, as: type) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) - } - - return value - } -} - -// MARK: - __DictionaryDecoder - -// NOTE: older overlays called this class _DictionaryDecoder. The two must -// coexist without a conflicting ObjC class name, so it was renamed. -// The old name must not be used in the new runtime. -private class __DictionaryDecoder: Decoder { - // MARK: Properties - - /// The decoder's storage. - fileprivate var storage: _DictionaryDecodingStorage - - /// Options set on the top-level decoder. - fileprivate let options: DictionaryDecoder._Options - - /// The path to the current point in encoding. - public fileprivate(set) var codingPath: [CodingKey] - - /// Contextual user-provided information for use during encoding. - public var userInfo: [CodingUserInfoKey: Any] { - return self.options.userInfo - } - - // MARK: - Initialization - - /// Initializes `self` with the given top-level container and options. - fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: DictionaryDecoder._Options) { - self.storage = _DictionaryDecodingStorage() - self.storage.push(container: container) - self.codingPath = codingPath - self.options = options - } - - // MARK: - Decoder Methods - - public func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - guard !(self.storage.topContainer is NSNull) else { - throw DecodingError.valueNotFound( - KeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get keyed decoding container -- found null value instead." - ) - ) - } - - guard let topContainer = self.storage.topContainer as? [String: Any] else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String: Any].self, reality: self.storage.topContainer) - } - - let container = _DictionaryKeyedDecodingContainer(referencing: self, wrapping: topContainer) - return KeyedDecodingContainer(container) - } - - public func unkeyedContainer() throws -> UnkeyedDecodingContainer { - guard !(self.storage.topContainer is NSNull) else { - throw DecodingError.valueNotFound( - UnkeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get unkeyed decoding container -- found null value instead." - ) - ) - } - - if let topContainer = self.storage.topContainer as? [Any] { - return _DictionaryUnkeyedDecodingContainer(referencing: self, wrapping: topContainer) - } else { - return _DictionaryUnkeyedDecodingContainer(referencing: self, wrapping: [self.storage.topContainer]) - } - } - - public func singleValueContainer() throws -> SingleValueDecodingContainer { - return self - } -} - -// MARK: - Decoding Storage - -private struct _DictionaryDecodingStorage { - // MARK: Properties - - /// The container stack. - /// Elements may be any one of the JSON types (NSNull, NSNumber, String, Array, [String : Any]). - fileprivate private(set) var containers: [Any] = [] - - // MARK: - Initialization - - /// Initializes `self` with no containers. - fileprivate init() {} - - // MARK: - Modifying the Stack - - fileprivate var count: Int { - return self.containers.count - } - - fileprivate var topContainer: Any { - precondition(!self.containers.isEmpty, "Empty container stack.") - return self.containers.last! - } - - fileprivate mutating func push(container: __owned Any) { - self.containers.append(container) - } - - fileprivate mutating func popContainer() { - precondition(!self.containers.isEmpty, "Empty container stack.") - self.containers.removeLast() - } -} - -// MARK: Decoding Containers - -private struct _DictionaryKeyedDecodingContainer: KeyedDecodingContainerProtocol { - typealias Key = K - - // MARK: Properties - - /// A reference to the decoder we're reading from. - private let decoder: __DictionaryDecoder - - /// A reference to the container we're reading from. - private let container: [String: Any] - - /// The path of coding keys taken to get to this point in decoding. - public private(set) var codingPath: [CodingKey] - - // MARK: - Initialization - - /// Initializes `self` by referencing the given decoder and container. - fileprivate init(referencing decoder: __DictionaryDecoder, wrapping container: [String: Any]) { - self.decoder = decoder - self.container = container - self.codingPath = decoder.codingPath - } - - // MARK: - KeyedDecodingContainerProtocol Methods - - public var allKeys: [Key] { - return self.container.keys.compactMap { Key(stringValue: $0) } - } - - public func contains(_ key: Key) -> Bool { - return self.container[key.stringValue] != nil - } - - private func _errorDescription(of key: CodingKey) -> String { - return "\(key) (\"\(key.stringValue)\")" - } - - public func decodeNil(forKey key: Key) throws -> Bool { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - return entry is NSNull - } - - public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Bool.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Int.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Int8.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Int16.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Int32.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Int64.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: UInt.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: UInt8.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: UInt16.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: UInt32.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: UInt64.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Float.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: Double.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: String.Type, forKey key: Key) throws -> String { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: String.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func decode(_ type: T.Type, forKey key: Key) throws -> T { - guard let entry = self.container[key.stringValue] else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key)).")) - } - - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = try self.decoder.unbox(entry, as: type) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) - } - - return value - } - - public func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = self.container[key.stringValue] else { - throw DecodingError.keyNotFound( - key, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get \(KeyedDecodingContainer.self) -- no value found for key \(_errorDescription(of: key))" - ) - ) - } - - guard let dictionary = value as? [String: Any] else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String: Any].self, reality: value) - } - - let container = _DictionaryKeyedDecodingContainer(referencing: self.decoder, wrapping: dictionary) - return KeyedDecodingContainer(container) - } - - public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - guard let value = self.container[key.stringValue] else { - throw DecodingError.keyNotFound( - key, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get UnkeyedDecodingContainer -- no value found for key \(_errorDescription(of: key))" - ) - ) - } - - if let array = value as? [Any] { - return _DictionaryUnkeyedDecodingContainer(referencing: self.decoder, wrapping: array) - } else { - return _DictionaryUnkeyedDecodingContainer(referencing: self.decoder, wrapping: [value]) - } - } - - private func _superDecoder(forKey key: __owned CodingKey) throws -> Decoder { - self.decoder.codingPath.append(key) - defer { self.decoder.codingPath.removeLast() } - - let value: Any = self.container[key.stringValue] ?? NSNull() - return __DictionaryDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options) - } - - public func superDecoder() throws -> Decoder { - return try _superDecoder(forKey: _DictionaryKey.super) - } - - public func superDecoder(forKey key: Key) throws -> Decoder { - return try _superDecoder(forKey: key) - } -} - -private struct _DictionaryUnkeyedDecodingContainer: UnkeyedDecodingContainer { - // MARK: Properties - - /// A reference to the decoder we're reading from. - private let decoder: __DictionaryDecoder - - /// A reference to the container we're reading from. - private let container: [Any] - - /// The path of coding keys taken to get to this point in decoding. - public private(set) var codingPath: [CodingKey] - - /// The index of the element we're about to decode. - public private(set) var currentIndex: Int - - // MARK: - Initialization - - /// Initializes `self` by referencing the given decoder and container. - fileprivate init(referencing decoder: __DictionaryDecoder, wrapping container: [Any]) { - self.decoder = decoder - self.container = container - self.codingPath = decoder.codingPath - self.currentIndex = 0 - } - - // MARK: - UnkeyedDecodingContainer Methods - - public var count: Int? { - return self.container.count - } - - public var isAtEnd: Bool { - return self.currentIndex >= self.count! - } - - public mutating func decodeNil() throws -> Bool { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(Any?.self, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - if self.container[self.currentIndex] is NSNull { - self.currentIndex += 1 - return true - } else { - return false - } - } - - public mutating func decode(_ type: Bool.Type) throws -> Bool { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Bool.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Int.Type) throws -> Int { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Int8.Type) throws -> Int8 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int8.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Int16.Type) throws -> Int16 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int16.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Int32.Type) throws -> Int32 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int32.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Int64.Type) throws -> Int64 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int64.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: UInt.Type) throws -> UInt { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: UInt.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: UInt8.Type) throws -> UInt8 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: UInt8.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: UInt16.Type) throws -> UInt16 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: UInt16.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: UInt32.Type) throws -> UInt32 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: UInt32.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: UInt64.Type) throws -> UInt64 { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: UInt64.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Float.Type) throws -> Float { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Float.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: Double.Type) throws -> Double { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Double.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: String.Type) throws -> String { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: String.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func decode(_ type: T.Type) throws -> T { - guard !self.isAtEnd else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) - } - - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: type) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_DictionaryKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead.")) - } - - self.currentIndex += 1 - return decoded - } - - public mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard !self.isAtEnd else { - throw DecodingError.valueNotFound( - KeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." - ) - ) - } - - let value = self.container[self.currentIndex] - guard !(value is NSNull) else { - throw DecodingError.valueNotFound( - KeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get keyed decoding container -- found null value instead." - ) - ) - } - - guard let dictionary = value as? [String: Any] else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String: Any].self, reality: value) - } - - self.currentIndex += 1 - let container = _DictionaryKeyedDecodingContainer(referencing: self.decoder, wrapping: dictionary) - return KeyedDecodingContainer(container) - } - - public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard !self.isAtEnd else { - throw DecodingError.valueNotFound( - UnkeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." - ) - ) - } - - let value = self.container[self.currentIndex] - guard !(value is NSNull) else { - throw DecodingError.valueNotFound( - UnkeyedDecodingContainer.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get keyed decoding container -- found null value instead." - ) - ) - } - - self.currentIndex += 1 - if let array = value as? [Any] { - return _DictionaryUnkeyedDecodingContainer(referencing: self.decoder, wrapping: array) - } else { - return _DictionaryUnkeyedDecodingContainer(referencing: self.decoder, wrapping: [value]) - } - } - - public mutating func superDecoder() throws -> Decoder { - self.decoder.codingPath.append(_DictionaryKey(index: self.currentIndex)) - defer { self.decoder.codingPath.removeLast() } - - guard !self.isAtEnd else { - throw DecodingError.valueNotFound( - Decoder.self, - DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Cannot get superDecoder() -- unkeyed container is at end." - ) - ) - } - - let value = self.container[self.currentIndex] - self.currentIndex += 1 - return __DictionaryDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options) - } -} - -extension __DictionaryDecoder: SingleValueDecodingContainer { - // MARK: SingleValueDecodingContainer Methods - - private func expectNonNull(_ type: T.Type) throws { - guard !self.decodeNil() else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected \(type) but found null value instead.")) - } - } - - func decodeNil() -> Bool { - return self.storage.topContainer is NSNull - } - - func decode(_: Bool.Type) throws -> Bool { - try expectNonNull(Bool.self) - return try self.unbox(self.storage.topContainer, as: Bool.self)! - } - - func decode(_: Int.Type) throws -> Int { - try expectNonNull(Int.self) - return try self.unbox(self.storage.topContainer, as: Int.self)! - } - - func decode(_: Int8.Type) throws -> Int8 { - try expectNonNull(Int8.self) - return try self.unbox(self.storage.topContainer, as: Int8.self)! - } - - func decode(_: Int16.Type) throws -> Int16 { - try expectNonNull(Int16.self) - return try self.unbox(self.storage.topContainer, as: Int16.self)! - } - - func decode(_: Int32.Type) throws -> Int32 { - try expectNonNull(Int32.self) - return try self.unbox(self.storage.topContainer, as: Int32.self)! - } - - func decode(_: Int64.Type) throws -> Int64 { - try expectNonNull(Int64.self) - return try self.unbox(self.storage.topContainer, as: Int64.self)! - } - - func decode(_: UInt.Type) throws -> UInt { - try expectNonNull(UInt.self) - return try self.unbox(self.storage.topContainer, as: UInt.self)! - } - - func decode(_: UInt8.Type) throws -> UInt8 { - try expectNonNull(UInt8.self) - return try self.unbox(self.storage.topContainer, as: UInt8.self)! - } - - func decode(_: UInt16.Type) throws -> UInt16 { - try expectNonNull(UInt16.self) - return try self.unbox(self.storage.topContainer, as: UInt16.self)! - } - - func decode(_: UInt32.Type) throws -> UInt32 { - try expectNonNull(UInt32.self) - return try self.unbox(self.storage.topContainer, as: UInt32.self)! - } - - func decode(_: UInt64.Type) throws -> UInt64 { - try expectNonNull(UInt64.self) - return try self.unbox(self.storage.topContainer, as: UInt64.self)! - } - - func decode(_: Float.Type) throws -> Float { - try expectNonNull(Float.self) - return try self.unbox(self.storage.topContainer, as: Float.self)! - } - - func decode(_: Double.Type) throws -> Double { - try expectNonNull(Double.self) - return try self.unbox(self.storage.topContainer, as: Double.self)! - } - - func decode(_: String.Type) throws -> String { - try expectNonNull(String.self) - return try self.unbox(self.storage.topContainer, as: String.self)! - } - - func decode(_ type: T.Type) throws -> T { - try expectNonNull(type) - return try self.unbox(self.storage.topContainer, as: type)! - } -} - -// MARK: - Concrete Value Representations - -extension __DictionaryDecoder { - @inline(__always) func unboxAsNumber(_ value: Any) -> NSNumber? { - if let number = value as? NSNumber { - return number - } - return (value as? HTTPHeaderDecodable)?.asNumber() - } - - /// Returns the given value unboxed from a container. - fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? { - guard !(value is NSNull) else { return nil } - - if let bool = value as? Bool { - return bool - } - if let bool = (value as? HTTPHeaderDecodable)?.asBool() { - return bool - } - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let int = number.intValue - guard NSNumber(value: int) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return int - } - - fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let int8 = number.int8Value - guard NSNumber(value: int8) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return int8 - } - - fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let int16 = number.int16Value - guard NSNumber(value: int16) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return int16 - } - - fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let int32 = number.int32Value - guard NSNumber(value: int32) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return int32 - } - - fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let int64 = number.int64Value - guard NSNumber(value: int64) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return int64 - } - - fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let uint = number.uintValue - guard NSNumber(value: uint) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return uint - } - - fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let uint8 = number.uint8Value - guard NSNumber(value: uint8) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return uint8 - } - - fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let uint16 = number.uint16Value - guard NSNumber(value: uint16) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return uint16 - } - - fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let uint32 = number.uint32Value - guard NSNumber(value: uint32) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return uint32 - } - - fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? { - guard !(value is NSNull) else { return nil } - - guard let number = unboxAsNumber(value) else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - let uint64 = number.uint64Value - guard NSNumber(value: uint64) == number else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number <\(number)> does not fit in \(type).")) - } - - return uint64 - } - - fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? { - guard !(value is NSNull) else { return nil } - - if let number = unboxAsNumber(value) { - // We are willing to return a Float by losing precision: - // * If the original value was integral, - // * and the integral value was > Float.greatestFiniteMagnitude, we will fail - // * and the integral value was <= Float.greatestFiniteMagnitude, we are willing to lose precision past 2^24 - // * If it was a Float, you will get back the precise value - // * If it was a Double or Decimal, you will get back the nearest approximation if it will fit - let double = number.doubleValue - guard abs(double) <= Double(Float.greatestFiniteMagnitude) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed number \(number) does not fit in \(type).")) - } - - return Float(double) - - /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: - } else if let double = value as? Double { - if abs(double) <= Double(Float.max) { - return Float(double) - } - - overflow = true - } else if let int = value as? Int { - if let float = Float(exactly: int) { - return float - } - - overflow = true - */ - - } else if let string = value as? String, - case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy - { - if string == posInfString { - return Float.infinity - } else if string == negInfString { - return -Float.infinity - } else if string == nanString { - return Float.nan - } - } - - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? { - guard !(value is NSNull) else { return nil } - - if let number = unboxAsNumber(value) { - // We are always willing to return the number as a Double: - // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double - // * If it was a Float or Double, you will get back the precise value - // * If it was Decimal, you will get back the nearest approximation - return number.doubleValue - - /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: - } else if let double = value as? Double { - return double - } else if let int = value as? Int { - if let double = Double(exactly: int) { - return double - } - - overflow = true - */ - - } else if let string = value as? String, - case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy - { - if string == posInfString { - return Double.infinity - } else if string == negInfString { - return -Double.infinity - } else if string == nanString { - return Double.nan - } - } - - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - fileprivate func unbox(_ value: Any, as type: String.Type) throws -> String? { - guard !(value is NSNull) else { return nil } - - if let string = value as? String { - return string - } - if let string = (value as? HTTPHeaderDecodable)?.asString() { - return string - } - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - fileprivate func unbox(_ value: Any, as type: Date.Type) throws -> Date? { - guard !(value is NSNull) else { return nil } - - switch self.options.dateDecodingStrategy { - case .deferredToDate: - self.storage.push(container: value) - defer { self.storage.popContainer() } - return try Date(from: self) - - case .secondsSince1970: - let double = try self.unbox(value, as: Double.self)! - return Date(timeIntervalSince1970: double) - - case .millisecondsSince1970: - let double = try self.unbox(value, as: Double.self)! - return Date(timeIntervalSince1970: double / 1000.0) - - case .formatted(let formatter): - let string = try self.unbox(value, as: String.self)! - guard let date = formatter.date(from: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Date string does not match format expected by formatter.")) - } - - return date - - case .custom(let closure): - self.storage.push(container: value) - defer { self.storage.popContainer() } - return try closure(self) - } - } - - fileprivate func unbox(_ value: Any, as type: Data.Type) throws -> Data? { - guard !(value is NSNull) else { return nil } - - switch self.options.dataDecodingStrategy { - case .base64: - guard let string = value as? String else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - guard let data = Data(base64Encoded: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Encountered Data is not valid Base64.")) - } - - return data - - case .raw: - guard let data = value as? Data else { - throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) - } - - return data - } - } - - fileprivate func unbox(_ value: Any, as type: T.Type) throws -> T? { - return try unbox_(value, as: type) as? T - } - - private func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? { - if type == Data.self { - return try self.unbox(value, as: Data.self) - } else if type == Date.self { - return try self.unbox(value, as: Date.self) - } else if type == AWSHTTPBody.self { - return value - } else { - self.storage.push(container: value) - defer { self.storage.popContainer() } - return try type.init(from: self) - } - } -} - -//===----------------------------------------------------------------------===// -// Shared Key Types -//===----------------------------------------------------------------------===// - -private struct _DictionaryKey: CodingKey { - public var stringValue: String - public var intValue: Int? - - public init?(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - public init?(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - - public init(stringValue: String, intValue: Int?) { - self.stringValue = stringValue - self.intValue = intValue - } - - fileprivate init(index: Int) { - self.stringValue = "Index \(index)" - self.intValue = index - } - - fileprivate static let `super` = _DictionaryKey(stringValue: "super")! -} - -//===----------------------------------------------------------------------===// -// Error Utilities -//===----------------------------------------------------------------------===// - -extension EncodingError { - /// Returns a `.invalidValue` error describing the given invalid floating-point value. - /// - /// - /// - parameter value: The value that was invalid to encode. - /// - parameter path: The path of `CodingKey`s taken to encode this value. - /// - returns: An `EncodingError` with the appropriate path and debug description. - fileprivate static func _invalidFloatingPointValue(_ value: T, at codingPath: [CodingKey]) -> EncodingError { - let valueDescription: String - if value == T.infinity { - valueDescription = "\(T.self).infinity" - } else if value == -T.infinity { - valueDescription = "-\(T.self).infinity" - } else { - valueDescription = "\(T.self).nan" - } - - let debugDescription = "Unable to encode \(valueDescription) directly. Use DictionaryEncoder.NonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded." - return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) - } -} - -internal extension DecodingError { - /// Returns a `.typeMismatch` error describing the expected type. - /// - /// - parameter path: The path of `CodingKey`s taken to decode a value of this type. - /// - parameter expectation: The type expected to be encountered. - /// - parameter reality: The value that was encountered instead of the expected type. - /// - returns: A `DecodingError` with the appropriate path and debug description. - static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError { - let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead." - return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) - } - - /// Returns a description of the type of `value` appropriate for an error message. - /// - /// - parameter value: The value whose type to describe. - /// - returns: A string describing `value`. - /// - precondition: `value` is one of the types below. - fileprivate static func _typeDescription(of value: Any) -> String { - if value is NSNull { - return "a null value" - } else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ { - return "a number" - } else if value is String { - return "a string/data" - } else if value is [Any] { - return "an array" - } else if value is [String: Any] { - return "a dictionary" - } else { - return "\(type(of: value))" - } - } -} - -/// Used to decode HTTP header values which could be either number, boolean or string -internal struct HTTPHeaderDecodable { - let value: String - - init(_ string: String) { - self.value = string - } - - func asBool() -> Bool? { - return Bool(value) - } - - func asString() -> String { - return value - } - - func asNumber() -> NSNumber? { - return NumberFormatter().number(from: value) - } -} diff --git a/Sources/SotoCore/Message/AWSResponse+HAL.swift b/Sources/SotoCore/Message/AWSResponse+HAL.swift index 628b1fa82..c0d4bcab2 100644 --- a/Sources/SotoCore/Message/AWSResponse+HAL.swift +++ b/Sources/SotoCore/Message/AWSResponse+HAL.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import struct Foundation.Data import class Foundation.JSONSerialization import NIOCore @@ -28,16 +29,15 @@ extension AWSResponse { } /// process hal+json data. Extract properties from HAL - func getHypertextApplicationLanguageDictionary() throws -> [String: Any] { - guard case .byteBuffer(let buffer) = self.body.storage else { return [:] } + func getHypertextApplicationLanguageDictionary() throws -> Data { + guard case .byteBuffer(let buffer) = self.body.storage else { return Data("{}".utf8) } // extract embedded resources from HAL - guard let data = buffer.getData(at: buffer.readerIndex, length: buffer.readableBytes) else { return [:] } - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - guard var dictionary = jsonObject as? [String: Any] else { return [:] } + let jsonObject = try JSONSerialization.jsonObject(with: buffer, options: []) + guard var dictionary = jsonObject as? [String: Any] else { return Data("{}".utf8) } guard let embedded = dictionary["_embedded"], let embeddedDictionary = embedded as? [String: Any] else { - return dictionary + return try JSONSerialization.data(withJSONObject: dictionary) } // remove _links and _embedded elements of dictionary to reduce the size of the new dictionary @@ -45,6 +45,6 @@ extension AWSResponse { dictionary["_embedded"] = nil // merge embedded resources into original dictionary dictionary.merge(embeddedDictionary) { first, _ in return first } - return dictionary + return try JSONSerialization.data(withJSONObject: dictionary) } } diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index 80131c07b..5307db2ec 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import AsyncHTTPClient +import struct Foundation.Data import struct Foundation.Date import class Foundation.DateFormatter import class Foundation.JSONDecoder @@ -66,139 +67,45 @@ public struct AWSResponse { /// Generate AWSShape from AWSResponse func generateOutputShape(operation: String, serviceProtocol: ServiceProtocol) throws -> Output { - var payloadKey: String? = (Output.self as? AWSShapeWithPayload.Type)?._payloadPath - - // if response has a payload with encoding info - if let payloadPath = payloadKey, let encoding = Output.getEncoding(for: payloadPath) { - // get CodingKey string for payload to insert in output - if let location = encoding.location, case .body(let name) = location { - payloadKey = name - } + var payload: ByteBuffer? = nil + if case .byteBuffer(let buffer) = self.body.storage { + payload = buffer } - let decoder = DictionaryDecoder() - decoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) - - var outputDict: [String: Any] = [:] - switch self.body.storage { - case .asyncSequence: - if let payloadKey = payloadKey { - outputDict[payloadKey] = self.body - } - // if body is raw or empty then assume any date to be decoded will be coming from headers - decoder.dateDecodingStrategy = .formatted(HTTPHeaderDateCoder.dateFormatters.first!) - case .byteBuffer(let buffer): - if buffer.readableBytes == 0 { - // we have no body so date decoding will be only HTTP headers - decoder.dateDecodingStrategy = .formatted(HTTPHeaderDateCoder.dateFormatters.first!) + switch serviceProtocol { + case .json, .restjson: + let payloadData: Data + if self.isHypertextApplicationLanguage { + payloadData = try self.getHypertextApplicationLanguageDictionary() + } else if let payload = payload, payload.readableBytes > 0 { + payloadData = Data(buffer: payload, byteTransferStrategy: .noCopy) } else { - switch serviceProtocol { - case .json, .restjson: - if let data = buffer.getData(at: buffer.readerIndex, length: buffer.readableBytes, byteTransferStrategy: .noCopy) { - // if required apply hypertext application language transform to body - if self.isHypertextApplicationLanguage { - outputDict = try self.getHypertextApplicationLanguageDictionary() - } else { - outputDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] - } - // if payload path is set then the decode will expect the payload to decode to the relevant member variable - /* if let payloadKey = payloadKey { - outputDict = [payloadKey: outputDict] - } */ - decoder.dateDecodingStrategy = .secondsSince1970 - } else { - // we have no body so date decoding will be only HTTP headers - decoder.dateDecodingStrategy = .formatted(HTTPHeaderDateCoder.dateFormatters.first!) - } - case .restxml, .query, .ec2: - let xmlDocument = try? XML.Document(buffer: buffer) - if let node = xmlDocument?.rootElement() { - var outputNode = node - // if payload path is set then the decode will expect the payload to decode to the relevant member variable. - // Most CloudFront responses have this. - /* if let payloadKey = payloadKey { - // set output node name - outputNode.name = payloadKey - // create parent node and add output node and set output node to parent - let parentNode = XML.Element(name: "Container") - parentNode.addChild(outputNode) - outputNode = parentNode - } else */ if let child = node.children(of: .element)?.first as? XML.Element, - node.name == operation + "Response", - child.name == operation + "Result" - { - outputNode = child - } - - // add headers to XML - // addHeadersToXML(rootElement: outputNode, output: Output.self) - - // add status code to XML - if let statusCodeParam = Output.statusCodeParam { - let node = XML.Element(name: statusCodeParam, stringValue: "\(self.status.code)") - outputNode.addChild(node) - } - var xmlDecoder = XMLDecoder() - xmlDecoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) - return try xmlDecoder.decode(Output.self, from: outputNode) - } - } - } - } - - // add headers to output dictionary - // outputDict = addHeadersToDictionary(dictionary: outputDict, output: Output.self) - - // add status code to output dictionary - if let statusCodeParam = Output.statusCodeParam { - outputDict[statusCodeParam] = self.status.code - } - - return try decoder.decode(Output.self, from: outputDict) - } - - /// Add headers required by Output type found in the response into dictionary to be decoded by Dictionary decoder - private func addHeadersToDictionary(dictionary: [String: Any], output: Output.Type) -> [String: Any] { - var dictionary = dictionary - // add header values to output dictionary, so they can be decoded into the response object - for (key, value) in self.headers { - let headerParams = Output.headerParams - if let index = headerParams.firstIndex(where: { $0.key.lowercased() == key.lowercased() }) { - dictionary[headerParams[index].key] = HTTPHeaderDecodable(value) - } - } - for param in Output.headerPrefixParams { - var valuesDict: [String: Any] = [:] - for (key, value) in self.headers { - guard key.lowercased().hasPrefix(param.key.lowercased()) else { continue } - let shortKey = String(key.dropFirst(param.key.count)) - valuesDict[shortKey] = HTTPHeaderDecodable(value) + payloadData = Data("{}".utf8) } - if valuesDict.count > 0 { - dictionary[param.key] = valuesDict - } - } - return dictionary - } - - /// Add headers required by Output type found in the response into xml to be decoded by xml decoder - private func addHeadersToXML(rootElement: XML.Element, output: Output.Type) { - // add header values to xmlnode as children nodes, so they can be decoded into the response object - for (key, value) in self.headers { - let headerParams = Output.headerParams - if let index = headerParams.firstIndex(where: { $0.key.lowercased() == key.lowercased() }) { - let node = XML.Element(name: headerParams[index].key, stringValue: value) - rootElement.addChild(node) + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .secondsSince1970 + jsonDecoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) + return try jsonDecoder.decode(Output.self, from: payloadData) + + case .restxml, .query, .ec2: + var xmlElement: XML.Element + if let buffer = payload, + let xmlDocument = try? XML.Document(buffer: buffer), + let rootElement = xmlDocument.rootElement() + { + xmlElement = rootElement + } else { + xmlElement = .init(name: "__empty_element") } - } - for param in Output.headerPrefixParams { - let parentNode = XML.Element(name: param.key) - rootElement.addChild(parentNode) - for (key, value) in self.headers { - guard key.hasPrefix(param.key) else { continue } - let entryNode = XML.Element(name: String(key.dropFirst(param.key.count)), stringValue: value) - parentNode.addChild(entryNode) + if let child = xmlElement.children(of: .element)?.first as? XML.Element, + xmlElement.name == operation + "Response", + child.name == operation + "Result" + { + xmlElement = child } + var xmlDecoder = XMLDecoder() + xmlDecoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) + return try xmlDecoder.decode(Output.self, from: xmlElement) } } diff --git a/Tests/SotoCoreTests/DictionaryEncoderTests.swift b/Tests/SotoCoreTests/DictionaryEncoderTests.swift deleted file mode 100644 index 778f4d5f8..000000000 --- a/Tests/SotoCoreTests/DictionaryEncoderTests.swift +++ /dev/null @@ -1,412 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2017-2022 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if compiler(>=5.7) && os(Linux) -@preconcurrency import struct Foundation.Data -#else -import struct Foundation.Data -#endif -@testable import SotoCore -import XCTest - -class DictionaryEncoderTests: XCTestCase { - func assertEqual(_ e1: Any, _ e2: Any) { - if let number1 = e1 as? NSNumber, let number2 = e2 as? NSNumber { - XCTAssertEqual(number1, number2) - } else if let string1 = e1 as? NSString, let string2 = e2 as? NSString { - XCTAssertEqual(string1, string2) - } else if let data1 = e1 as? Data, let data2 = e2 as? Data { - XCTAssertEqual(data1.base64EncodedString(), data2.base64EncodedString()) - } else if let date1 = e1 as? NSDate, let date2 = e2 as? NSDate { - XCTAssertEqual(date1, date2) - } else if let d1 = e1 as? [String: Any], let d2 = e2 as? [String: Any] { - self.assertDictionaryEqual(d1, d2) - } else if let a1 = e1 as? [Any], let a2 = e2 as? [Any] { - self.assertArrayEqual(a1, a2) - } else if let desc1 = e1 as? CustomStringConvertible, let desc2 = e2 as? CustomStringConvertible { - XCTAssertEqual(desc1.description, desc2.description) - } - } - - func assertArrayEqual(_ a1: [Any], _ a2: [Any]) { - XCTAssertEqual(a1.count, a2.count) - for i in 0..( - type: T.Type, - dictionary: [String: Any], - decoder: DictionaryDecoder = DictionaryDecoder(), - test: (T) -> Void - ) { - do { - let instance = try decoder.decode(T.self, from: dictionary) - test(instance) - } catch { - XCTFail("\(error)") - } - } - - func testSimpleStructureDecodeEncode() { - struct Test: Codable { - let a: Int - let b: String - } - let dictionary: [String: Any] = ["a": 4, "b": "Hello"] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a, 4) - XCTAssertEqual($0.b, "Hello") - } - } - - func testBaseTypesDecodeEncode() { - struct Test: Codable { - let bool: Bool - let int: Int - let int8: Int8 - let int16: Int16 - let int32: Int32 - let int64: Int64 - let uint: UInt - let uint8: UInt8 - let uint16: UInt16 - let uint32: UInt32 - let uint64: UInt64 - let float: Float - let double: Double - } - let dictionary: [String: Any] = ["bool": true, "int": 0, "int8": 1, "int16": 2, "int32": -3, "int64": 4, "uint": 10, "uint8": 11, "uint16": 12, "uint32": 13, "uint64": 14, "float": 1.25, "double": 0.23] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.bool, true) - XCTAssertEqual($0.int, 0) - XCTAssertEqual($0.int8, 1) - XCTAssertEqual($0.int16, 2) - XCTAssertEqual($0.int32, -3) - XCTAssertEqual($0.int64, 4) - XCTAssertEqual($0.uint, 10) - XCTAssertEqual($0.uint8, 11) - XCTAssertEqual($0.uint16, 12) - XCTAssertEqual($0.uint32, 13) - XCTAssertEqual($0.uint64, 14) - XCTAssertEqual($0.float, 1.25) - XCTAssertEqual($0.double, 0.23) - } - } - - func testContainingStructureDecodeEncode() { - struct Test2: AWSDecodableShape { - let a: Int - let b: String - } - struct Test: AWSDecodableShape { - let t: Test2 - } - let dictionary: [String: Any] = ["t": ["a": 4, "b": "Hello"] as [String: Any]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.t.a, 4) - XCTAssertEqual($0.t.b, "Hello") - } - } - - func testEnumDecodeEncode() { - struct Test: AWSDecodableShape { - enum TestEnum: String, Codable { - case first = "First" - case second = "Second" - } - - let a: TestEnum - } - let dictionary: [String: Any] = ["a": "First"] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a, .first) - } - } - - func testArrayDecodeEncode() { - struct Test: AWSDecodableShape { - let a: [Int] - } - let dictionary: [String: Any] = ["a": [1, 2, 3, 4, 5]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a, [1, 2, 3, 4, 5]) - } - } - - func testArrayOfStructuresDecodeEncode() { - struct Test2: AWSDecodableShape { - let b: String - } - struct Test: AWSDecodableShape { - let a: [Test2] - } - let dictionary: [String: Any] = ["a": [["b": "hello"], ["b": "goodbye"]]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a[0].b, "hello") - XCTAssertEqual($0.a[1].b, "goodbye") - } - } - - func testDictionaryDecodeEncode() { - struct Test: AWSDecodableShape { - let a: [String: Int] - } - let dictionary: [String: Any] = ["a": ["key": 45]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a["key"], 45) - } - } - - func testEnumDictionaryDecodeEncode() { - struct Test: AWSDecodableShape { - enum TestEnum: String, Codable { - case first = "First" - case second = "Second" - } - - let a: [TestEnum: Int] - } - // at the moment dictionaries with enums return an array. - let dictionary: [String: Any] = ["a": ["First", 45] as [Any]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.a[.first], 45) - } - } - - func testDataDecodeEncode() { - struct Test: AWSDecodableShape { - let data: Data - } - let dictionary: [String: Any] = ["data": "Hello, world".data(using: .utf8)!.base64EncodedString()] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.data, "Hello, world".data(using: .utf8)!) - } - - let decoder = DictionaryDecoder() - decoder.dataDecodingStrategy = .raw - - let dictionary2: [String: Any] = ["data": "Hello, world".data(using: .utf8)!] - self.testDecode(type: Test.self, dictionary: dictionary2, decoder: decoder) { - XCTAssertEqual($0.data, "Hello, world".data(using: .utf8)!) - } - } - - func testDecodeErrors(type: T.Type, dictionary: [String: Any], decoder: DictionaryDecoder = DictionaryDecoder()) { - do { - _ = try DictionaryDecoder().decode(T.self, from: dictionary) - XCTFail("Decoder did not throw an error when it should have") - } catch {} - } - - func testFloatOverflowDecodeErrors() { - struct Test: AWSDecodableShape { - let float: Float - } - let dictionary: [String: Any] = ["float": Double.infinity] - self.testDecodeErrors(type: Test.self, dictionary: dictionary) - } - - func testMissingKeyDecodeErrors() { - struct Test: AWSDecodableShape { - let a: Int - let b: Int - } - let dictionary: [String: Any] = ["b": 1] - self.testDecodeErrors(type: Test.self, dictionary: dictionary) - } - - func testInvalidValueDecodeErrors() { - struct Test: AWSDecodableShape { - let a: Int - } - let dictionary: [String: Any] = ["b": "test"] - self.testDecodeErrors(type: Test.self, dictionary: dictionary) - } - - func testNestedContainer() { - struct Test: AWSDecodableShape { - let firstname: String - let surname: String - let age: Int - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.age = try container.decode(Int.self, forKey: .age) - let fullname = try container.nestedContainer(keyedBy: AdditionalKeys.self, forKey: .name) - self.firstname = try fullname.decode(String.self, forKey: .firstname) - self.surname = try fullname.decode(String.self, forKey: .surname) - } - - private enum CodingKeys: String, CodingKey { - case name - case age - } - - private enum AdditionalKeys: String, CodingKey { - case firstname - case surname - } - } - - let dictionary: [String: Any] = ["age": 25, "name": ["firstname": "John", "surname": "Smith"]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.age, 25) - XCTAssertEqual($0.firstname, "John") - XCTAssertEqual($0.surname, "Smith") - } - } - - func testSupercoder() { - class Base: Decodable { - let a: Int - } - class Test: Base { - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.b = try container.decode(String.self, forKey: .b) - let superDecoder = try container.superDecoder(forKey: .super) - try super.init(from: superDecoder) - } - - let b: String - - private enum CodingKeys: String, CodingKey { - case b = "B" - case `super` = "Super" - } - } - - let dictionary: [String: Any] = ["B": "Test", "Super": ["a": 648]] - self.testDecode(type: Test.self, dictionary: dictionary) { - XCTAssertEqual($0.b, "Test") - XCTAssertEqual($0.a, 648) - } - } - - struct B: AWSDecodableShape { - let int: Int - let int8: Int8 - let int16: Int16 - let int32: Int32 - let int64: Int64 - let uint: UInt - let uint8: UInt8 - let uint16: UInt16 - let uint32: UInt32 - let uint64: UInt64 - let double: Double - let float: Float - let string: String - let data: Data - let bool: Bool - let optional: String? - } - - struct A: AWSDecodableShape { - let b: B - let dictionary: [String: String] - let array: [String] - } - - func testDecode() { - do { - let dictionary: [String: Any] = [ - "b": [ - "int": Int.max, - "int8": -Int8.max, - "int16": -Int16.max, - "int32": -Int32.max, - "int64": -Int64.max, - "uint": UInt.max, - "uint8": UInt8.max, - "uint16": UInt16.max, - "uint32": UInt32.max, - "uint64": UInt64.max, - "double": Double.greatestFiniteMagnitude, - "float": Float.greatestFiniteMagnitude, - "string": "hello", - "data": "hello".data(using: .utf8)!.base64EncodedString(), - "bool": true, - "optional": "hello", - ] as [String: Any], - "dictionary": ["foo": "bar"], - "array": ["a", "b", "c"], - ] - - let a = try DictionaryDecoder().decode(A.self, from: dictionary) - - XCTAssertEqual(a.b.int, 9_223_372_036_854_775_807) - XCTAssertEqual(a.b.int8, -127) - XCTAssertEqual(a.b.int16, -32767) - XCTAssertEqual(a.b.int32, -2_147_483_647) - XCTAssertEqual(a.b.int64, -9_223_372_036_854_775_807) - XCTAssertEqual(a.b.uint, 18_446_744_073_709_551_615) - XCTAssertEqual(a.b.uint8, 255) - XCTAssertEqual(a.b.uint16, 65535) - XCTAssertEqual(a.b.uint32, 4_294_967_295) - XCTAssertEqual(a.b.uint64, 18_446_744_073_709_551_615) - XCTAssertEqual(a.b.double, 1.7976931348623157e+308) - XCTAssertEqual(a.b.float, 3.40282347e+38) - XCTAssertEqual(a.b.string, "hello") - XCTAssertEqual(a.b.data, "hello".data(using: .utf8)) - XCTAssertEqual(a.b.bool, true) - XCTAssertEqual(a.b.optional, "hello") - XCTAssertEqual(a.dictionary, ["foo": "bar"]) - XCTAssertEqual(a.array, ["a", "b", "c"]) - - } catch { - XCTFail("\(error)") - } - } - - func testDecodeFail() { - do { - let dictionary: [String: Any] = [ - "b": [ - "int": 1, - "double": 2.0, - "float": 3.0, - "string": "hello", - "data": "hello".data(using: .utf8)!, - "bool": true, - "optional": "hello", - ] as [String: Any], - "dictionary": ["foo": "bar"], - "array": ["a", "b", "c"], - ] - - _ = try DictionaryDecoder().decode(A.self, from: dictionary) - XCTFail("Never reached here") - - } catch DecodingError.keyNotFound(let key, _) { - XCTAssertEqual(key.stringValue, "int8") - } catch { - XCTFail("\(error)") - } - } -} diff --git a/Tests/SotoCoreTests/JSONCoderTests.swift b/Tests/SotoCoreTests/JSONCoderTests.swift deleted file mode 100644 index ef7fdaeb0..000000000 --- a/Tests/SotoCoreTests/JSONCoderTests.swift +++ /dev/null @@ -1,202 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2017-2020 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore -@testable import SotoCore -import XCTest - -class JSONCoderTests: XCTestCase { - struct Numbers: AWSDecodableShape & AWSEncodableShape { - init(bool: Bool, integer: Int, float: Float, double: Double, intEnum: IntEnum) { - self.bool = bool - self.integer = integer - self.float = float - self.double = double - self.intEnum = intEnum - self.int8 = 4 - self.uint16 = 5 - self.int32 = 7 - self.uint64 = 90 - } - - enum IntEnum: Int, Codable { - case first - case second - case third - } - - let bool: Bool - let integer: Int - let float: Float - let double: Double - let intEnum: IntEnum - let int8: Int8 - let uint16: UInt16 - let int32: Int32 - let uint64: UInt64 - - private enum CodingKeys: String, CodingKey { - case bool = "b" - case integer = "i" - case float = "s" - case double = "d" - case intEnum = "enum" - case int8 - case uint16 - case int32 - case uint64 - } - } - - struct StringShape: AWSDecodableShape & AWSEncodableShape { - enum StringEnum: String, Codable { - case first - case second - case third - case fourth - } - - let string: String - let optionalString: String? - let stringEnum: StringEnum - } - - struct Arrays: AWSDecodableShape & AWSEncodableShape { - let arrayOfNatives: [Int] - let arrayOfShapes: [Numbers] - } - - struct Dictionaries: AWSDecodableShape & AWSEncodableShape { - let dictionaryOfNatives: [String: Int] - let dictionaryOfShapes: [String: StringShape] - - private enum CodingKeys: String, CodingKey { - case dictionaryOfNatives = "natives" - case dictionaryOfShapes = "shapes" - } - } - - struct Shape: AWSDecodableShape & AWSEncodableShape { - let numbers: Numbers - let stringShape: StringShape - let arrays: Arrays - - private enum CodingKeys: String, CodingKey { - case numbers = "Numbers" - case stringShape = "Strings" - case arrays = "Arrays" - } - } - - struct ShapeWithDictionaries: AWSDecodableShape & AWSEncodableShape { - let shape: Shape - let dictionaries: Dictionaries - - private enum CodingKeys: String, CodingKey { - case shape = "s" - case dictionaries = "d" - } - } - - var testShape: Shape { - return Shape( - numbers: Numbers(bool: true, integer: 45, float: 3.4, double: 7.89234, intEnum: .second), - stringShape: StringShape(string: "String1", optionalString: "String2", stringEnum: .third), - arrays: Arrays(arrayOfNatives: [34, 1, 4098], arrayOfShapes: [Numbers(bool: false, integer: 1, float: 1.2, double: 1.4, intEnum: .first), Numbers(bool: true, integer: 3, float: 2.01, double: 1.01, intEnum: .third)]) - ) - } - - var testShapeWithDictionaries: ShapeWithDictionaries { - return ShapeWithDictionaries(shape: self.testShape, dictionaries: Dictionaries( - dictionaryOfNatives: ["first": 1, "second": 2, "third": 3], - dictionaryOfShapes: [ - "strings": StringShape(string: "one", optionalString: "two", stringEnum: .third), - "strings2": StringShape(string: "cat", optionalString: nil, stringEnum: .fourth), - ] - )) - } - - /// helper test function to use throughout all the decode/encode tests - func testEncodeDecode(object: T, expected: String) { - do { - let jsonData = try JSONEncoder().encode(object) - XCTAssertEqual(jsonData, Data(expected.utf8)) - let dict = try JSONSerialization.jsonObject(with: jsonData, options: []) - let object2 = try DictionaryDecoder().decode(T.self, from: dict) - XCTAssertEqual(object, object2) - } catch { - XCTFail("\(error)") - } - } - - func testSerializeToDictionaryAndJSON() throws { - var json = try self.testShapeWithDictionaries.encodeAsJSON(byteBufferAllocator: ByteBufferAllocator()) - let data = try XCTUnwrap(json.readData(length: json.readableBytes)) - let dict = try! JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] - - let dict2 = dict["s"] as? [String: Any] - - // Member scalar - let stringsDict = dict2?["Strings"]! as? [String: Any] - let string = stringsDict?["string"] as? String - XCTAssertEqual(string, "String1") - - // array member - let arrayDict = dict2?["Arrays"] as? [String: Any] - let nativeArray = arrayDict?["arrayOfNatives"] as? [Any] - let value = nativeArray?[2] as? Int - XCTAssertEqual(value, 4098) - - // dicationary member - let dictionaryDict = dict["d"] as? [String: Any] - let dictionaryOfShapes = dictionaryDict?["shapes"] as? [String: Any] - let stringsDict2 = dictionaryOfShapes?["strings"] as? [String: Any] - let stringEnum = stringsDict2?["stringEnum"] as? String - XCTAssertEqual(stringEnum, "third") - - do { - let shape2 = try DictionaryDecoder().decode(ShapeWithDictionaries.self, from: dict) - XCTAssertEqual(shape2.shape.numbers.intEnum, .second) - } catch { - XCTFail(error.localizedDescription) - } - } - - func testBase64() { - struct Test: Codable, Equatable { - let data: AWSBase64Data - } - self.testEncodeDecode(object: Test(data: .data("Testing".utf8)), expected: #"{"data":"VGVzdGluZw=="}"#) - } - - func testBase64EncodeDecode() throws { - struct Test: Codable, Equatable { - let data: AWSBase64Data - } - // self.testEncodeDecode(object: Test(data: .data("Testing".utf8)), expected: #"{"data":"VGVzdGluZw=="}"#) - let string = """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, - quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse - cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat - non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - """ - let test = Test(data: .data(string.utf8)) - let jsonData = try JSONEncoder().encode(test) - let dict = try JSONSerialization.jsonObject(with: jsonData, options: []) - let object2 = try DictionaryDecoder().decode(Test.self, from: dict) - XCTAssertEqual([UInt8](string.utf8), object2.data.decoded()) - } -} From c9c4ee59e3f7ca3d84b78e557617c3825360d94c Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 29 Jun 2023 13:30:52 +0100 Subject: [PATCH 07/10] Fix tests --- Tests/SotoCoreTests/AWSResponseTests.swift | 16 +++++++++++++--- Tests/SotoCoreTests/PayloadTests.swift | 5 +++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Tests/SotoCoreTests/AWSResponseTests.swift b/Tests/SotoCoreTests/AWSResponseTests.swift index afd6cb86d..562661efb 100644 --- a/Tests/SotoCoreTests/AWSResponseTests.swift +++ b/Tests/SotoCoreTests/AWSResponseTests.swift @@ -89,6 +89,10 @@ class AWSResponseTests: XCTestCase { struct Output: AWSDecodableShape { static let _encoding = [AWSMemberEncoding(label: "status", location: .statusCode)] let status: Int + public init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.status = response.decodeStatus() + } } let response = AWSHTTPResponse( status: .ok, @@ -157,6 +161,11 @@ class AWSResponseTests: XCTestCase { static let _payloadPath: String = "body" static let _options: AWSShapeOptions = .rawPayload let body: AWSHTTPBody + + init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.body = response.decodePayload() + } } let byteBuffer = ByteBuffer(string: "{\"name\":\"hello\"}") let response = AWSHTTPResponse( @@ -219,10 +228,11 @@ class AWSResponseTests: XCTestCase { struct Output: AWSDecodableShape, AWSShapeWithPayload { static let _payloadPath: String = "body" static let _options: AWSShapeOptions = .rawPayload - public static var _encoding = [ - AWSMemberEncoding(label: "contentType", location: .header("content-type")), - ] let body: AWSHTTPBody + init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.body = response.decodePayload() + } } let byteBuffer = ByteBuffer(string: "{\"name\":\"hello\"}") let response = AWSHTTPResponse( diff --git a/Tests/SotoCoreTests/PayloadTests.swift b/Tests/SotoCoreTests/PayloadTests.swift index cc90c4b64..7ae41f9d2 100644 --- a/Tests/SotoCoreTests/PayloadTests.swift +++ b/Tests/SotoCoreTests/PayloadTests.swift @@ -74,6 +74,11 @@ class PayloadTests: XCTestCase { static let _payloadPath: String = "payload" static let _options: AWSShapeOptions = .rawPayload let payload: AWSHTTPBody + + init(from decoder: Decoder) throws { + let response = decoder.userInfo[.awsResponse] as! ResponseDecodingContainer + self.payload = response.decodePayload() + } } do { let awsServer = AWSTestServer(serviceProtocol: .json) From b8e0d7c6e13e4b88df642d664f23792b5e192f8c Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 29 Jun 2023 13:53:20 +0100 Subject: [PATCH 08/10] Re-organise root element code --- Sources/SotoCore/Message/AWSResponse.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index 5307db2ec..a33e087e8 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -94,15 +94,18 @@ public struct AWSResponse { let rootElement = xmlDocument.rootElement() { xmlElement = rootElement + // if root element is called operation name + "Response" and its child is called + // operation name + "Result" then use the child as the root element when decoding + // XML + if let child = xmlElement.children(of: .element)?.first as? XML.Element, + xmlElement.name == operation + "Response", + child.name == operation + "Result" + { + xmlElement = child + } } else { xmlElement = .init(name: "__empty_element") } - if let child = xmlElement.children(of: .element)?.first as? XML.Element, - xmlElement.name == operation + "Response", - child.name == operation + "Result" - { - xmlElement = child - } var xmlDecoder = XMLDecoder() xmlDecoder.userInfo[.awsResponse] = ResponseDecodingContainer(response: self) return try xmlDecoder.decode(Output.self, from: xmlElement) From 6c708e4faf147b583f866bdc9fa15efd6e67c8b9 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 14 Jul 2023 10:45:42 +0100 Subject: [PATCH 09/10] Fix after merge --- Sources/SotoCore/Encoder/ResponseContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SotoCore/Encoder/ResponseContainer.swift b/Sources/SotoCore/Encoder/ResponseContainer.swift index ca0d16a15..3e19c3286 100644 --- a/Sources/SotoCore/Encoder/ResponseContainer.swift +++ b/Sources/SotoCore/Encoder/ResponseContainer.swift @@ -54,7 +54,7 @@ public struct ResponseDecodingContainer { return Value(self.response.status.code) } - public func decodePayload() -> HTTPBody { + public func decodePayload() -> AWSHTTPBody { return self.response.body } From 7869961d559337ad8d994cf42ffdaa13dadc0685 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 18 Jul 2023 07:58:16 +0100 Subject: [PATCH 10/10] Update Sources/SotoCore/Encoder/ResponseContainer.swift Co-authored-by: Tim Condon <0xTim@users.noreply.github.com> --- Sources/SotoCore/Encoder/ResponseContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SotoCore/Encoder/ResponseContainer.swift b/Sources/SotoCore/Encoder/ResponseContainer.swift index 3e19c3286..bcac9ff3a 100644 --- a/Sources/SotoCore/Encoder/ResponseContainer.swift +++ b/Sources/SotoCore/Encoder/ResponseContainer.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information