Skip to content
13 changes: 11 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down Expand Up @@ -74,6 +75,12 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
]
),
.target(
name: "SmithySerde",
dependencies: [
"Smithy",
]
),
.target(
name: "ClientRuntime",
dependencies: [
Expand Down Expand Up @@ -137,6 +144,7 @@ let package = Package(
.target(
name: "SmithyXML",
dependencies: [
"SmithySerde",
"SmithyReadWrite",
"SmithyTimestamps",
libXML2DependencyOrNil
Expand All @@ -145,6 +153,7 @@ let package = Package(
.target(
name: "SmithyJSON",
dependencies: [
"SmithySerde",
"SmithyReadWrite",
"SmithyTimestamps"
]
Expand Down Expand Up @@ -284,7 +293,7 @@ let package = Package(
),
.testTarget(
name: "SmithyXMLTests",
dependencies: ["SmithyXML", "ClientRuntime"]
dependencies: ["SmithySerde", "SmithyXML", "ClientRuntime"]
),
.testTarget(
name: "SmithyHTTPAuthTests",
Expand All @@ -296,7 +305,7 @@ let package = Package(
),
.testTarget(
name: "SmithyJSONTests",
dependencies: ["SmithyJSON", "ClientRuntime", "SmithyTestUtil"]
dependencies: ["SmithySerde", "SmithyJSON", "ClientRuntime", "SmithyTestUtil"]
),
.testTarget(
name: "SmithyFormURLTests",
Expand Down
18 changes: 13 additions & 5 deletions Sources/SmithyJSON/Reader/Reader+JSONDeserialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
15 changes: 15 additions & 0 deletions Sources/SmithySerde/InvalidEncodingError.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 5 additions & 1 deletion Sources/SmithyXML/Reader/Reader+libxml2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import struct Foundation.Data
@preconcurrency import libxml2
import struct SmithySerde.InvalidEncodingError

extension Reader {

Expand All @@ -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() }
Expand Down Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,20 @@ class OrchestratorTests: XCTestCase {
.attributes(attributes)
.serialize({ input, builder, _ in
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests below used JSON fragments (i.e. a JSON string only) for serde since fragments were allowed and it kept the test setup simpler. Tests now serialize JSON objects.

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")
if (200..<300).contains(response.statusCode.rawValue) {
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)
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 14 additions & 3 deletions Tests/SmithyJSONTests/ReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
}
Loading