Add nested keypath support

This commit is contained in:
Alexandr Goncharov 2017-10-08 04:09:45 +03:00
parent 1f431d11fa
commit 0245520fc2
3 changed files with 120 additions and 12 deletions

View File

@ -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
}

View File

@ -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)
]
}

View File

@ -2,5 +2,5 @@ import XCTest
@testable import JSONDecoder_KeypathTests
XCTMain([
testCase(JSONDecoder_KeypathTests.allTests)
testCase(JSONDecoderKeypathTests.allTests)
])