Skip to content

Commit a4fc54e

Browse files
committed
Add a helper method JSONError.withPrefixedCodingPath(_:)
This is intended to be used when using `JSONError`-throwing methods in a `Decodable` implementation, in order to prepend the `Decoder`'s `codingPath` to the error.
1 parent e5891bf commit a4fc54e

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
265265
#### Development
266266

267267
* Add convenience property `JSONError.path`.
268+
* Add method `JSONError.withPrefixedCodingPath(_:)` to make it easier to use `JSONError`-throwing methods in a `Decodable` implementation.
268269

269270
#### v3.0.1 (2018-02-18)
270271

Sources/JSONError.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,40 @@ public enum JSONError: Error, CustomStringConvertible {
6262
}
6363
}
6464

65+
/// A helper method to modify the error path based on a `Decoder` coding path.
66+
///
67+
/// This is meant to be used when using `PMJSON` accessors in a `Decodable.init(from:)`
68+
/// implementation. It produces a new `JSONError` that prepends the given coding path to the
69+
/// error's existing path. This can be convenient when writing a `Decodable` adapter for an
70+
/// existing type that already can initialize itself from a `JSONObject. For example:
71+
///
72+
/// init(from decoder: Decoder) throws {
73+
/// var container = try decoder.singleValueContainer()
74+
/// let object = try container.decode(JSONObject.self)
75+
/// do {
76+
/// try self.init(json: object)
77+
/// } catch let error as JSONError {
78+
/// throw error.withPrefixedCodingPath(decoder.codingPath)
79+
/// }
80+
/// }
81+
///
82+
/// - Parameter codingPath: A coding path. This should be taken from `decoder.codingPath`.
83+
/// - Returns: A new `JSONError` that matches the receiver but with a new path.
84+
public func withPrefixedCodingPath(_ codingPath: [CodingKey]) -> JSONError {
85+
var prefix = ""
86+
for key in codingPath {
87+
if let intValue = key.intValue {
88+
prefix += "[\(intValue)]"
89+
} else {
90+
if !prefix.isEmpty {
91+
prefix += "."
92+
}
93+
prefix += key.stringValue
94+
}
95+
}
96+
return prefix.isEmpty ? self : withPrefix(prefix)
97+
}
98+
6599
fileprivate func withPrefix(_ prefix: String) -> JSONError {
66100
func prefixPath(_ path: String?, with prefix: String) -> String {
67101
guard let path = path, !path.isEmpty else { return prefix }

Tests/PMJSONTests/SwiftCodableTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,70 @@ final class SwiftDecodableTests: XCTestCase {
122122
}
123123
}
124124

125+
final class SwiftMiscellaneousCodableTests: XCTestCase {
126+
private enum TestKey: CodingKey {
127+
case int(Int)
128+
case string(String)
129+
130+
init?(intValue: Int) {
131+
self = .int(intValue)
132+
}
133+
134+
init?(stringValue: String) {
135+
self = .string(stringValue)
136+
}
137+
138+
var intValue: Int? {
139+
switch self {
140+
case .int(let x): return x
141+
case .string: return nil
142+
}
143+
}
144+
145+
var stringValue: String {
146+
switch self {
147+
case .int(let x): return String(x)
148+
case .string(let s): return s
149+
}
150+
}
151+
}
152+
153+
func testErrorWithEmptyPrefixedCodingPath() {
154+
let error = JSONError.missingOrInvalidType(path: "x", expected: .required(.number), actual: nil)
155+
XCTAssertEqual(error.withPrefixedCodingPath([]).path, "x")
156+
}
157+
158+
func testErrorWithSingleStringPrefixedCodingPath() {
159+
let error = JSONError.outOfRangeInt64(path: "x", value: 32760, expected: Int8.self)
160+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.string("foo")]).path, "foo.x")
161+
}
162+
163+
func testErrorWithMultipleStringPrefixedCodingPath() {
164+
let error = JSONError.outOfRangeDouble(path: "[1]", value: 32760, expected: Int8.self)
165+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.string("foo"), TestKey.string("bar")]).path, "foo.bar[1]")
166+
}
167+
168+
func testErrorWithSingleIntPrefixedCodingPath() {
169+
let error = JSONError.outOfRangeDecimal(path: "x", value: 32760, expected: Int8.self)
170+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.int(42)]).path, "[42].x")
171+
}
172+
173+
func testErrorWithMultipleIntPrefixedCodingPath() {
174+
let error = JSONError.missingOrInvalidType(path: "x", expected: .required(.number), actual: nil)
175+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.int(2), TestKey.int(42)]).path, "[2][42].x")
176+
}
177+
178+
func testErrorWithMixedPrefixedCodignPath() {
179+
let error = JSONError.missingOrInvalidType(path: "x", expected: .required(.number), actual: nil)
180+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.string("foo"), TestKey.int(42), TestKey.string("bar")]).path, "foo[42].bar.x")
181+
}
182+
183+
func testErrorWithNilPathPrefixedCodingPath() {
184+
let error = JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: nil)
185+
XCTAssertEqual(error.withPrefixedCodingPath([TestKey.string("foo"), TestKey.string("bar")]).path, "foo.bar")
186+
}
187+
}
188+
125189
/// Wrapper to make JSONEncoder/JSONDecoder happy about encoding values.
126190
private struct ValueWrapper: Codable {
127191
let value: JSON

0 commit comments

Comments
 (0)