Skip to content

Commit

Permalink
feat!: XML response deserialization (#1299)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbelkins authored Feb 10, 2024
1 parent 92b1e2a commit 368026e
Show file tree
Hide file tree
Showing 69 changed files with 1,267 additions and 1,597 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ final class S3StreamTests: S3XCTestCase {
case .data(let dataOrNil):
let data = try XCTUnwrap(dataOrNil)
let actual = String(data: data, encoding: .utf8)
XCTAssertEqual(actual, expected)
XCTAssertEqual(expected, actual)
case .stream(let stream):
let actual = String(data: try await stream.readToEndAsync()!, encoding: .utf8)
XCTAssertEqual(actual, expected)
let actual = String(data: try await stream.readToEndAsync() ?? Data(), encoding: .utf8)
XCTAssertEqual(expected, actual)
case .noStream:
XCTFail("Expected stream")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class S3XCTestCase: XCTestCase {
let data = try XCTUnwrap(dataOrNil)
return String(data: data, encoding: .utf8)
case .stream(let stream):
return String(data: try await stream.readToEndAsync()!, encoding: .utf8)
return String(data: try await stream.readToEndAsync() ?? Data(), encoding: .utf8)
case .noStream:
return nil
}
Expand Down
27 changes: 14 additions & 13 deletions Sources/Core/AWSClientRuntime/Errors/RestXMLError+AWS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@
*/

import ClientRuntime
import class SmithyXML.Reader

extension RestXMLError {

/// Makes a `RestXMLError` from the provided `HttpResponse`.
/// If the response body is empty and the status code is "not-found" aka 404, then this returns a `RestXMLError` instance with an error code of "NotFound".
/// Otherwise, it creates an instance of `RestXMLError` by calling ``RestXMLError.init(httpResponse: HttpResponse)``.
///
/// - Parameter response: The HTTP response
///
/// - Returns: A`RestXMLError` instance with an error code of "NotFound" if the response body is empty and the status code is 404. Otherwise returns a `RestXMLError` by calling ``RestXMLError.init(httpResponse: HttpResponse)``.
///
/// - Parameter httpResponse: The HTTP response from the server.
/// - Parameter responseReader: The Reader created from the XML response body.
/// - Parameter noErrorWrapping: `true` if the error is wrapped in a XML `ErrorResponse` element, `false` otherwise.
/// - Returns: A`RestXMLError` instance with an error code of "NotFound" if the response body is empty and the status code is 404, else returns a `RestXMLError` by calling the `RestXMLError` initializer.
/// - Throws: An error if it fails to decode the response body.
public static func makeError(from response: HttpResponse) async throws -> RestXMLError {
response.statusCodeIsNotFoundAndBodyIsEmpty
? .makeNotFoundError(requestID: response.requestId)
: try await .init(httpResponse: response)
}

static func makeNotFoundError(requestID: String?) -> RestXMLError {
return RestXMLError(errorCode: "NotFound", requestId: requestID)
public static func makeError(
from httpResponse: HttpResponse,
responseReader: SmithyXML.Reader,
noErrorWrapping: Bool
) async throws -> RestXMLError {
return httpResponse.statusCodeIsNotFoundAndBodyIsEmpty
? .init(code: "NotFound", message: "404 Not Found", requestID: httpResponse.requestId)
: try .init(responseReader: responseReader, noErrorWrapping: noErrorWrapping)
}
}
19 changes: 13 additions & 6 deletions Sources/Core/AWSClientRuntime/Protocols/Ec2Query/Ec2Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
// SPDX-License-Identifier: Apache-2.0
//

public struct Ec2Error: Decodable {
public let code: String
public let message: String
import SmithyReadWrite
import SmithyXML

enum CodingKeys: String, CodingKey {
case code = "Code"
case message = "Message"
public struct Ec2Error {
public var code: String?
public var message: String?

static var readingClosure: ReadingClosure<Ec2Error, Reader> {
return { reader in
var value = Ec2Error()
value.code = try reader["Code"].readIfPresent()
value.message = try reader["Message"].readIfPresent()
return value
}
}
}
15 changes: 11 additions & 4 deletions Sources/Core/AWSClientRuntime/Protocols/Ec2Query/Ec2Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
// SPDX-License-Identifier: Apache-2.0
//

public struct Ec2Errors: Decodable {
public let error: Ec2Error
import SmithyReadWrite
import SmithyXML

enum CodingKeys: String, CodingKey {
case error = "Error"
public struct Ec2Errors {
public var error: Ec2Error?

static var readingClosure: ReadingClosure<Ec2Errors, Reader> {
return { reader in
var value = Ec2Errors()
value.error = try reader["Error"].readIfPresent(readingClosure: Ec2Error.readingClosure)
return value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,19 @@
// SPDX-License-Identifier: Apache-2.0
//

import ClientRuntime
import class ClientRuntime.HttpResponse
import class SmithyXML.Reader
import var ClientRuntime.responseDocumentBinding

public struct Ec2QueryError {
public let errorCode: String?
public let requestId: String?
public let message: String?
public var errorCode: String?
public var requestId: String?
public var message: String?

public init(httpResponse: HttpResponse) async throws {
guard let data = try await httpResponse.body.readData() else {
errorCode = nil
requestId = nil
message = nil
return
}
let decoded: Ec2Response = try XMLDecoder().decode(responseBody: data)
self.errorCode = decoded.errors.error.code
self.message = decoded.errors.error.message
self.requestId = decoded.requestId
let response = try await Ec2Response.httpBinding(httpResponse, responseDocumentBinding)
self.errorCode = response.errors?.error?.code
self.message = response.errors?.error?.message
self.requestId = response.requestId
}
}
30 changes: 14 additions & 16 deletions Sources/Core/AWSClientRuntime/Protocols/Ec2Query/Ec2Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@
// SPDX-License-Identifier: Apache-2.0
//

public struct Ec2Response: Decodable {
public let errors: Ec2Errors
public let requestId: String
import SmithyReadWrite
import SmithyXML
@testable import ClientRuntime

enum CodingKeys: String, CodingKey {
case errors = "Errors"
case requestId = "RequestId"
case requestID = "RequestID"
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.errors = try container.decode(Ec2Errors.self, forKey: .errors)
public struct Ec2Response {
public var errors: Ec2Errors?
public var requestId: String?

// Attempt to decode the requestId with the key "RequestID"
// if that is not present, then fallback to the key "RequestId"
self.requestId = try container.decodeIfPresent(String.self, forKey: .requestID)
?? container.decode(String.self, forKey: .requestId)
public static var httpBinding: HTTPResponseOutputBinding<Ec2Response, Reader> {
return { httpResponse, responseDocumentBinding in
let reader = try await responseDocumentBinding(httpResponse)
var value = Ec2Response()
value.errors = try reader["Errors"].readIfPresent(readingClosure: Ec2Errors.readingClosure)
value.requestId = try reader["RequestId"].readIfPresent() ?? reader["RequestID"].readIfPresent()
return value
}
}
}
48 changes: 24 additions & 24 deletions Sources/Core/AWSClientRuntime/Protocols/RestXML/RestXMLError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@
// SPDX-License-Identifier: Apache-2.0
//

import ClientRuntime
import class SmithyXML.Reader

public struct RestXMLError {
public let errorCode: String?
public let requestId: String?
public let code: String
public let message: String?
public let requestID: String?

public init(httpResponse: HttpResponse) async throws {
guard let data = try await httpResponse.body.readData() else {
errorCode = nil
requestId = nil
message = nil
return
}
do {
let decoded: ErrorResponseContainer<RestXMLErrorPayload>
decoded = try XMLDecoder().decode(responseBody: data)
self.errorCode = decoded.error.errorCode
self.message = decoded.error.message
self.requestId = decoded.requestId
} catch {
let decoded: RestXMLErrorNoErrorWrappingPayload = try XMLDecoder().decode(responseBody: data)
self.errorCode = decoded.errorCode
self.message = decoded.message
self.requestId = decoded.requestId
public static func errorBodyReader(responseReader: Reader, noErrorWrapping: Bool) -> Reader {
noErrorWrapping ? responseReader : responseReader["Error"]
}

public init(responseReader: Reader, noErrorWrapping: Bool) throws {
let reader = Self.errorBodyReader(responseReader: responseReader, noErrorWrapping: noErrorWrapping)
let code: String? = try reader["Code"].readIfPresent()
let message: String? = try reader["Message"].readIfPresent()
let requestID: String? = try responseReader["RequestId"].readIfPresent()
guard let code else {
throw RestXMLDecodeError.missingRequiredData
}
self.code = code
self.message = message
self.requestID = requestID
}

public init(errorCode: String? = nil, requestId: String? = nil, message: String? = nil) {
self.errorCode = errorCode
self.requestId = requestId
public init(code: String, message: String?, requestID: String?) {
self.code = code
self.message = message
self.requestID = requestID
}
}

public enum RestXMLDecodeError: Error {
case missingRequiredData
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ final class AWSMessageDecoderStreamTests: XCTestCase {
let bufferedStream = BufferedStream(data: validMessageDataWithAllHeaders + validMessageDataEmptyPayload + validMessageDataNoHeaders,
isClosed: true)
let messageDecoder = AWSEventStream.AWSMessageDecoder()
let sut = EventStream.DefaultMessageDecoderStream<TestEvent>(stream: bufferedStream,
messageDecoder: messageDecoder,
responseDecoder: JSONDecoder())
let sut = EventStream.DefaultMessageDecoderStream<TestEvent>(
stream: bufferedStream,
messageDecoder: messageDecoder,
unmarshalClosure: jsonUnmarshalClosure(responseDecoder: JSONDecoder())
)

var events: [TestEvent] = []
for try await evnt in sut {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Sha256TreeHashMiddlewareTests: XCTestCase {
var stack = OperationStack<MockStreamInput, MockOutput>(id: "TreeHashMiddlewareTestStack")
stack.serializeStep.intercept(position: .before, middleware: MockSerializeStreamMiddleware())
let mockHttpResponse = HttpResponse(body: .noStream, statusCode: .accepted)
let mockOutput = try MockOutput(httpResponse: mockHttpResponse, decoder: nil)
let mockOutput = MockOutput()
let output = OperationOutput<MockOutput>(httpResponse: mockHttpResponse, output: mockOutput)
stack.finalizeStep.intercept(position: .after, middleware: Sha256TreeHashMiddleware<MockOutput>())
_ = try await stack.handleMiddleware(context: context, input: streamInput, next: MockHandler(handleCallback: { context, input in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import ClientRuntime
import SmithyTestUtil
import XCTest
import SmithyXML
@testable import AWSClientRuntime

class Ec2ErrorRequestIdTests: XCTestCase {

func testEc2ResponseDecodesRequestID() throws {
let data = """
func testEc2ResponseDecodesRequestID() async throws {
let data = Data("""
<Ec2Response>
<Errors>
<Error>
Expand All @@ -24,13 +25,14 @@ class Ec2ErrorRequestIdTests: XCTestCase {
</Errors>
<RequestID>abcdefg12345</RequestID>
</Ec2Response>
""".data(using: .utf8)!
let response = try XMLDecoder().decode(Ec2Response.self, from: data)
""".utf8)
let httpResponse = HttpResponse(body: .data(data), statusCode: .ok)
let response = try await responseClosure(Ec2Response.httpBinding, responseDocumentBinding)(httpResponse)
XCTAssertEqual(response.requestId, "abcdefg12345")
}

func testEc2ResponseDecodesRequestId() throws {
let data = """
func testEc2ResponseDecodesRequestId() async throws {
let data = Data("""
<Ec2Response>
<Errors>
<Error>
Expand All @@ -40,34 +42,37 @@ class Ec2ErrorRequestIdTests: XCTestCase {
</Errors>
<RequestId>abcdefg12345</RequestId>
</Ec2Response>
""".data(using: .utf8)!
let response = try XMLDecoder().decode(Ec2Response.self, from: data)
""".utf8)
let httpResponse = HttpResponse(body: .data(data), statusCode: .ok)
let response = try await responseClosure(Ec2Response.httpBinding, responseDocumentBinding)(httpResponse)
XCTAssertEqual(response.requestId, "abcdefg12345")
}

func testEc2NarrowedResponseDecodesRequestID() throws {
let data = """
func testEc2NarrowedResponseDecodesRequestID() async throws {
let data = Data("""
<Ec2NarrowedResponse>
<Errors>
<Error>Sample Error</Error>
</Errors>
<RequestID>abcdefg12345</RequestID>
</Ec2NarrowedResponse>
""".data(using: .utf8)!
let response = try XMLDecoder().decode(Ec2NarrowedResponse<String>.self, from: data)
""".utf8)
let httpResponse = HttpResponse(body: .data(data), statusCode: .ok)
let response = try await responseClosure(Ec2Response.httpBinding, responseDocumentBinding)(httpResponse)
XCTAssertEqual(response.requestId, "abcdefg12345")
}

func testEc2NarrowResponseDecodesRequestId() throws {
let data = """
func testEc2NarrowResponseDecodesRequestId() async throws {
let data = Data("""
<Ec2Response>
<Errors>
<Error>Sample Error</Error>
</Errors>
<RequestId>abcdefg12345</RequestId>
</Ec2Response>
""".data(using: .utf8)!
let response = try XMLDecoder().decode(Ec2NarrowedResponse<String>.self, from: data)
""".utf8)
let httpResponse = HttpResponse(body: .data(data), statusCode: .ok)
let response = try await responseClosure(Ec2Response.httpBinding, responseDocumentBinding)(httpResponse)
XCTAssertEqual(response.requestId, "abcdefg12345")
}
}

This file was deleted.

Loading

0 comments on commit 368026e

Please sign in to comment.