diff --git a/Package.swift b/Package.swift index 755b40342..1bae2d369 100644 --- a/Package.swift +++ b/Package.swift @@ -30,6 +30,7 @@ let package = Package( ], products: [ .library(name: "Smithy", targets: ["Smithy"]), + .library(name: "SmithySerde", targets: ["SmithySerde"]), .library(name: "ClientRuntime", targets: ["ClientRuntime"]), .library(name: "SmithyRetriesAPI", targets: ["SmithyRetriesAPI"]), .library(name: "SmithyRetries", targets: ["SmithyRetries"]), @@ -74,6 +75,12 @@ let package = Package( .product(name: "Logging", package: "swift-log"), ] ), + .target( + name: "SmithySerde", + dependencies: [ + "Smithy", + ] + ), .target( name: "ClientRuntime", dependencies: [ @@ -137,6 +144,7 @@ let package = Package( .target( name: "SmithyXML", dependencies: [ + "SmithySerde", "SmithyReadWrite", "SmithyTimestamps", libXML2DependencyOrNil @@ -145,6 +153,7 @@ let package = Package( .target( name: "SmithyJSON", dependencies: [ + "SmithySerde", "SmithyReadWrite", "SmithyTimestamps" ] @@ -284,7 +293,7 @@ let package = Package( ), .testTarget( name: "SmithyXMLTests", - dependencies: ["SmithyXML", "ClientRuntime"] + dependencies: ["SmithySerde", "SmithyXML", "ClientRuntime"] ), .testTarget( name: "SmithyHTTPAuthTests", @@ -296,7 +305,7 @@ let package = Package( ), .testTarget( name: "SmithyJSONTests", - dependencies: ["SmithyJSON", "ClientRuntime", "SmithyTestUtil"] + dependencies: ["SmithySerde", "SmithyJSON", "ClientRuntime", "SmithyTestUtil"] ), .testTarget( name: "SmithyFormURLTests", diff --git a/Sources/SmithyJSON/Reader/Reader+JSONDeserialization.swift b/Sources/SmithyJSON/Reader/Reader+JSONDeserialization.swift index 8f44ce0be..ff4e4aa2c 100644 --- a/Sources/SmithyJSON/Reader/Reader+JSONDeserialization.swift +++ b/Sources/SmithyJSON/Reader/Reader+JSONDeserialization.swift @@ -9,16 +9,24 @@ import struct Foundation.Data import class Foundation.JSONSerialization import class Foundation.NSError import class Foundation.NSNull +import struct SmithySerde.InvalidEncodingError extension Reader { public static func from(data: Data) throws -> Reader { - guard !data.isEmpty else { return Reader(nodeInfo: "", parent: nil) } + // Empty bodies are allowed. When the body is empty, + // return a reader with no JSON content. + guard !data.isEmpty else { return try Reader(nodeInfo: "", jsonObject: [:]) } + + // Attempt to parse JSON from the non-empty body. + // Throw an error if JSON is invalid. + // (Determine whether to wrap this error) + let jsonObject: Any do { - let jsonObject = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) - return try Reader(nodeInfo: "", jsonObject: jsonObject) - } catch let error as NSError where error.domain == "NSCocoaErrorDomain" && error.code == 3840 { - return try Reader(nodeInfo: "", jsonObject: [:]) + jsonObject = try JSONSerialization.jsonObject(with: data) + } catch { + throw InvalidEncodingError(wrapped: error) } + return try Reader(nodeInfo: "", jsonObject: jsonObject) } } diff --git a/Sources/SmithySerde/InvalidEncodingError.swift b/Sources/SmithySerde/InvalidEncodingError.swift new file mode 100644 index 000000000..4edfc9ee8 --- /dev/null +++ b/Sources/SmithySerde/InvalidEncodingError.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct InvalidEncodingError: Error { + public var localizedDescription: String { "The data in the response could not be parsed" } + public let wrapped: Error // this will be the underlying error thrown by the parser implementation + + public init(wrapped: Error) { + self.wrapped = wrapped + } +} diff --git a/Sources/SmithyXML/Reader/Reader+libxml2.swift b/Sources/SmithyXML/Reader/Reader+libxml2.swift index 3ae5c35f2..1913cacd3 100644 --- a/Sources/SmithyXML/Reader/Reader+libxml2.swift +++ b/Sources/SmithyXML/Reader/Reader+libxml2.swift @@ -7,6 +7,7 @@ import struct Foundation.Data @preconcurrency import libxml2 +import struct SmithySerde.InvalidEncodingError extension Reader { @@ -21,7 +22,9 @@ extension Reader { guard let buffer else { return Reader() } // Read the buffer into a XML document tree - guard let doc = xmlReadMemory(buffer.pointee.content, Int32(count), "", "UTF-8", 0) else { return Reader() } + guard let doc = xmlReadMemory(buffer.pointee.content, Int32(count), "", "UTF-8", 0) else { + throw InvalidEncodingError(wrapped: XMLError.parsingError) + } // Get rootNode ptr. Just a ptr to inside the doc struct, so no memory allocated guard let rootNode = xmlDocGetRootElement(doc) else { return Reader() } @@ -154,4 +157,5 @@ private struct XMLError: Error { static let memoryError = XMLError("XML buffer could not be allocated") static let invalidNode = XMLError("XML node was invalid") static let invalidNodeName = XMLError("XML node name was invalid") + static let parsingError = XMLError("The XML could not be parsed") } diff --git a/Tests/ClientRuntimeTests/OrchestratorTests/OrchestratorTests.swift b/Tests/ClientRuntimeTests/OrchestratorTests/OrchestratorTests.swift index 8cb8135df..55c463568 100644 --- a/Tests/ClientRuntimeTests/OrchestratorTests/OrchestratorTests.swift +++ b/Tests/ClientRuntimeTests/OrchestratorTests/OrchestratorTests.swift @@ -223,10 +223,11 @@ class OrchestratorTests: XCTestCase { .attributes(attributes) .serialize({ input, builder, _ in trace.append("serialize") + let data = try JSONEncoder().encode(["foo": input.foo]) builder.withMethod(.get) .withPath("/") .withHost("localhost") - .withBody(.data(try! JSONEncoder().encode(input.foo))) + .withBody(.data(data)) }) .deserialize({ response, _ in trace.append("deserialize") @@ -234,8 +235,8 @@ class OrchestratorTests: XCTestCase { guard case let .data(data) = response.body else { return TestOutput(bar: "") } - let bar = try! JSONDecoder().decode(String.self, from: data!) - return TestOutput(bar: bar) + let object = try! JSONDecoder().decode([String: String].self, from: data!) + return TestOutput(bar: object["foo"]!) } else { let responseReader = try SmithyJSON.Reader.from(data: try await response.data()) let baseError = try TestBaseError(httpResponse: response, responseReader: responseReader, noErrorWrapping: true) @@ -1373,7 +1374,8 @@ class OrchestratorTests: XCTestCase { let orchestrator = traceOrchestrator(trace: trace) .retryStrategy(DefaultRetryStrategy(options: RetryStrategyOptions(backoffStrategy: ImmediateBackoffStrategy()))) .serialize({ (input: TestInput, builder: HTTPRequestBuilder, context) in - builder.withBody(.data(Data("\"\(input.foo)\"".utf8))) + let data = try JSONEncoder().encode(["foo": input.foo]) + builder.withBody(.data(data)) }) .executeRequest(executeRequest) let result = await asyncResult { diff --git a/Tests/SmithyJSONTests/ReaderTests.swift b/Tests/SmithyJSONTests/ReaderTests.swift index 76a10b30f..797ad7a51 100644 --- a/Tests/SmithyJSONTests/ReaderTests.swift +++ b/Tests/SmithyJSONTests/ReaderTests.swift @@ -6,14 +6,16 @@ // import XCTest +import struct SmithySerde.InvalidEncodingError @testable @_spi(SmithyReadWrite) import SmithyJSON -class ReaderTests: XCTestCase { +final class ReaderTests: XCTestCase { - func test_readsNil() async throws { + func test_readsEmptyDataAsEmptyJSONObject() async throws { let jsonData = Data() let reader = try SmithyJSON.Reader.from(data: jsonData) - XCTAssertEqual(reader.jsonNode, nil) + XCTAssertEqual(reader.jsonNode, .object) + XCTAssert(reader.children.isEmpty) } func test_readsAJSONObject() async throws { @@ -24,4 +26,13 @@ class ReaderTests: XCTestCase { XCTAssertEqual(reader.children.count, 1) XCTAssertEqual(try reader["property"].readIfPresent(), "potato") } + + func test_throwsOnInvalidJSON() async throws { + let jsonData = Data(""" + { "json": "incomplet + """.utf8) + XCTAssertThrowsError(try SmithyJSON.Reader.from(data: jsonData)) { error in + XCTAssert(error is InvalidEncodingError) + } + } }