-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: XML response deserialization #1299
Changes from all commits
3e78b74
cf9bb15
569d5de
34a52d4
d7a592b
1b7e7b9
2562b24
5bae0ee
4f2b458
a0aba18
0711195
4d327a1
5a3cb57
4de9974
3ba40ad
ca01cb4
833360c
d1304ad
b99378a
fe2fb19
491ef6d
de4a4ec
d8beae9
5a25bbf
90ffa69
e638413
aec6f45
837bb87
4f483bf
d8f3a81
9d8e96d
0168c60
65bfcf4
1341b98
f2a2899
bf13ee8
48bd940
372806a
1abe22d
de92168
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,24 +4,25 @@ | |
*/ | ||
|
||
import ClientRuntime | ||
import class SmithyXML.Reader | ||
|
||
extension RestXMLError { | ||
|
||
/// Makes a `RestXMLError` from the provided `HttpResponse`. | ||
sichanyoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert this error customization to use a XML reader |
||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to use Reader to get error fields instead of Decoder |
||
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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as with previous type
sichanyoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert this type to use a XML reader |
||
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uses a closure binding instead of the Decodable protocol |
||
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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,37 +5,37 @@ | |
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import ClientRuntime | ||
import class SmithyXML.Reader | ||
|
||
public struct RestXMLError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracts common fields from Rest XML errors. Can read from "unwrapped" errors as well. |
||
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 |
---|---|---|
|
@@ -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()) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converted to use closures for event streams. |
||
|
||
var events: [TestEvent] = [] | ||
for try await evnt in sut { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
@@ -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> | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests for EC2 errors above are converted to use closures instead of decoders to create the errors. The tests for the several following deleted files were removed because they appear to have been copied from protocol tests. I suspect they were used in the early days of establishing protocol tests in this project. The same tests still exist in the protocol tests suite, and they are being removed here as redundant. |
||
XCTAssertEqual(response.requestId, "abcdefg12345") | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactoring