Add nested keypath support
This commit is contained in:
parent
1f431d11fa
commit
0245520fc2
|
@ -8,16 +8,20 @@ public extension JSONDecoder {
|
|||
/// - type: The type of the value to decode.
|
||||
/// - data: The data to decode from.
|
||||
/// - keyPath: The JSON keypath
|
||||
/// - keyPathSeparator: Nested keypath separator
|
||||
/// - Returns: A value of the requested type.
|
||||
/// - Throws: An error if any value throws an error during decoding.
|
||||
func decode<T>(_ type: T.Type, from data: Data, keyPath: String) throws -> T where T : Decodable {
|
||||
userInfo[keyPathUserInfoKey] = keyPath
|
||||
func decode<T>(_ type: T.Type,
|
||||
from data: Data,
|
||||
keyPath: String,
|
||||
keyPathSeparator separator: String = ".") throws -> T where T : Decodable {
|
||||
userInfo[keyPathUserInfoKey] = keyPath.components(separatedBy: separator)
|
||||
return try decode(KeyPathWrapper<T>.self, from: data).object
|
||||
}
|
||||
}
|
||||
|
||||
/// The keypath key in the `userInfo`
|
||||
private let keyPathUserInfoKey = CodingUserInfoKey(rawValue: "keyPathUserInfoKey")!
|
||||
private let keyPathUserInfoKey = CodingUserInfoKey(rawValue: "keyPathUserInfoKey")! //swiftlint:disable:this force_unwrapping
|
||||
|
||||
/// Object which is representing value
|
||||
private final class KeyPathWrapper<T: Decodable>: Decodable {
|
||||
|
@ -42,14 +46,36 @@ private final class KeyPathWrapper<T: Decodable>: Decodable {
|
|||
let stringValue: String
|
||||
}
|
||||
|
||||
typealias KeyedContainer = KeyedDecodingContainer<KeyPathWrapper<T>.Key>
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
guard let keyPath = decoder.userInfo[keyPathUserInfoKey] as? String,
|
||||
let key = Key(stringValue: keyPath)
|
||||
guard let keyPath = decoder.userInfo[keyPathUserInfoKey] as? [String],
|
||||
!keyPath.isEmpty
|
||||
else { throw KeyPathError.internal }
|
||||
|
||||
let keyedContainer = try decoder.container(keyedBy: Key.self)
|
||||
/// Creates a `Key` from the first keypath element
|
||||
func getKey(from keyPath: [String]) throws -> Key {
|
||||
guard let first = keyPath.first,
|
||||
let key = Key(stringValue: first)
|
||||
else { throw KeyPathError.internal }
|
||||
return key
|
||||
}
|
||||
|
||||
/// Finds nested container and returns it and the key for object
|
||||
func objectContainer(for keyPath: [String],
|
||||
in currentContainer: KeyedContainer,
|
||||
key currentKey: Key) throws -> (KeyedContainer, Key) {
|
||||
guard !keyPath.isEmpty else { return (currentContainer, currentKey) }
|
||||
let container = try currentContainer.nestedContainer(keyedBy: Key.self, forKey: currentKey)
|
||||
let key = try getKey(from: keyPath)
|
||||
return try objectContainer(for: Array(keyPath.dropFirst()), in: container, key: key)
|
||||
}
|
||||
|
||||
let rootKey = try getKey(from: keyPath)
|
||||
let rooTContainer = try decoder.container(keyedBy: Key.self)
|
||||
let (keyedContainer, key) = try objectContainer(for: Array(keyPath.dropFirst()), in: rooTContainer, key: rootKey)
|
||||
object = try keyedContainer.decode(T.self, forKey: key)
|
||||
}
|
||||
|
||||
let object: T
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import XCTest
|
||||
@testable import JSONDecoder_Keypath
|
||||
|
||||
//swiftlint:disable force_unwrapping
|
||||
struct Item: Codable, Equatable {
|
||||
let title: String
|
||||
let number: Int
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.number == rhs.number
|
||||
}
|
||||
}
|
||||
|
||||
class JSONDecoder_KeypathTests: XCTestCase {
|
||||
class JSONDecoderKeypathTests: XCTestCase {
|
||||
var decoder: JSONDecoder!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
decoder = JSONDecoder()
|
||||
}
|
||||
|
||||
|
@ -63,9 +64,90 @@ class JSONDecoder_KeypathTests: XCTestCase {
|
|||
XCTAssertThrowsError(try decoder.decode(Item.self, from: jsonData, keyPath: "custom"))
|
||||
}
|
||||
|
||||
func testObjectArray() {
|
||||
let json = """
|
||||
{
|
||||
"custom": [
|
||||
{
|
||||
"title" : "Item title",
|
||||
"number" : 2
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
let jsonData = json.data(using: .utf8)!
|
||||
do {
|
||||
let array = try decoder.decode([Item].self, from: jsonData, keyPath: "custom")
|
||||
let object = array.first
|
||||
XCTAssertNotNil(object)
|
||||
XCTAssertEqual(Item(title: "Item title", number: 2), object!)
|
||||
} catch let error {
|
||||
XCTFail("Error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testValidNestedKeypath() {
|
||||
let json = """
|
||||
{
|
||||
"level1": {
|
||||
"level2": {
|
||||
"title" : "Item title",
|
||||
"number" : 2
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
let jsonData = json.data(using: .utf8)!
|
||||
do {
|
||||
let object = try decoder.decode(Item.self, from: jsonData, keyPath: "level1.level2")
|
||||
XCTAssertEqual(Item(title: "Item title", number: 2), object)
|
||||
} catch let error {
|
||||
XCTFail("Error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testInvalidNestedKeypath() {
|
||||
let json = """
|
||||
{
|
||||
"level1": {
|
||||
"level2": {
|
||||
"title" : "Item title",
|
||||
"number" : 2
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
let jsonData = json.data(using: .utf8)!
|
||||
XCTAssertThrowsError(try decoder.decode(Item.self, from: jsonData, keyPath: "level1.invalid"))
|
||||
}
|
||||
|
||||
func testCustomSeparator() {
|
||||
let json = """
|
||||
{
|
||||
"level1": {
|
||||
"level2": {
|
||||
"title" : "Item title",
|
||||
"number" : 2
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
let jsonData = json.data(using: .utf8)!
|
||||
do {
|
||||
let object = try decoder.decode(Item.self, from: jsonData, keyPath: "level1/level2", keyPathSeparator: "/")
|
||||
XCTAssertEqual(Item(title: "Item title", number: 2), object)
|
||||
} catch let error {
|
||||
XCTFail("Error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("Test Valid Decoding", testValidDecoding),
|
||||
("Test Invalid Keypath", testValidDecoding),
|
||||
("Test Invalid Object", testValidDecoding)
|
||||
("Test Invalid Keypath", testInvalidKeyPath),
|
||||
("Test Invalid Object", testInvalidObject),
|
||||
("Test Object Array", testObjectArray),
|
||||
("Test Valid Nested Keypath", testValidNestedKeypath),
|
||||
("Test InValid Nested Keypath", testInvalidNestedKeypath),
|
||||
("Test Custom Separator", testCustomSeparator)
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ import XCTest
|
|||
@testable import JSONDecoder_KeypathTests
|
||||
|
||||
XCTMain([
|
||||
testCase(JSONDecoder_KeypathTests.allTests)
|
||||
testCase(JSONDecoderKeypathTests.allTests)
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue