diff --git a/README.md b/README.md index a1259a4..7f90714 100644 --- a/README.md +++ b/README.md @@ -233,18 +233,18 @@ You can also use key paths (for both dictionary keys and array indexes) to unbox ```json { - "name": "John", - "age": 27, - "activities": { - "running": { - "distance": 300 - } - }, - "devices": [ - "Macbook Pro", - "iPhone", - "iPad" - ] + "name": "John", + "age": 27, + "activities": { + "running": { + "distance": 300 + } + }, + "devices": [ + "Macbook Pro", + "iPhone", + "iPad" + ] } ``` @@ -272,41 +272,41 @@ You can also use key paths to directly unbox nested JSON structures. This is use { "company": { "name": "Spotify", - }, - "jobOpenings": [ - { - "title": "Swift Developer", - "salary": 120000 - }, - { - "title": "UI Designer", - "salary": 100000 - }, - ] + }, + "jobOpenings": [ + { + "title": "Swift Developer", + "salary": 120000 + }, + { + "title": "UI Designer", + "salary": 100000 + }, + ] } ``` ```swift struct JobOpening { - let title: String - let salary: Int + let title: String + let salary: Int } extension JobOpening: Unboxable { - init(unboxer: Unboxer) { - self.title = unboxer.unbox("title") - self.salary = unboxer.unbox("salary") - } + init(unboxer: Unboxer) { + self.title = unboxer.unbox("title") + self.salary = unboxer.unbox("salary") + } } struct Company { - let name: String + let name: String } extension Company: Unboxable { - init(unboxer: Unboxer) { - self.name = unboxer.unbox("name") - } + init(unboxer: Unboxer) { + self.name = unboxer.unbox("name") + } } ``` diff --git a/Sources/Unbox.swift b/Sources/Unbox.swift index b4020fe..bed33a3 100644 --- a/Sources/Unbox.swift +++ b/Sources/Unbox.swift @@ -41,15 +41,17 @@ public func Unbox(dictionary: UnboxableDictionary, context: Any? = } /// Unbox a JSON dictionary into a model `T` beginning at a provided key, optionally using a contextual object. Throws `UnboxError`. -public func Unbox(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true, context: Any? = nil) throws -> T { - return try Unboxer.unboxer(at: key, in: dictionary, isKeyPath: isKeyPath, context: context).performUnboxing() +public func Unbox(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true) throws -> T { + let context = UnboxContainerContext(key: key, isKeyPath: isKeyPath) + let container: UnboxContainer = try Unbox(dictionary, context: context) + return container.model } -/// Unbox an array JSON dictionary into a model `T` beginning at a provided key, optionally using a contextual object and/or invalid elements. Throws `UnboxError`. -public func Unbox(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true, context: Any? = nil, allowInvalidElements: Bool = false) throws -> [T] { - return try Unboxer.unboxers(at: key, in: dictionary, isKeyPath: isKeyPath, context: context).mapAllowingInvalidElements(allowInvalidElements, transform: { - return try $0.performUnboxing() - }) +/// Unbox an array JSON dictionary into an array of model `T` beginning at a provided key, optionally using a contextual object and/or invalid elements. Throws `UnboxError`. +public func Unbox(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true) throws -> [T] { + let context = UnboxContainerContext(key: key, isKeyPath: isKeyPath) + let container: UnboxArrayContainer = try Unbox(dictionary, context: context) + return container.models } /// Unbox an array of JSON dictionaries into an array of `T`, optionally using a contextual object and/or invalid elements. Throws `UnboxError`. @@ -786,26 +788,40 @@ private class UnboxValueResolver { } func resolveOptionalValueForKey(key: String, isKeyPath: Bool, transform: T -> R?) -> R? { - do { - let values = try Unboxer.retrieveValues(from: self.unboxer.dictionary, at: key, isKeyPath: isKeyPath) - let dictionary = values.dictionary - let array = values.array - let modifiedKey = values.modifiedKey - - if let value = dictionary[modifiedKey] as? T { - if let transformed = transform(value) { - return transformed - } - } else if let index = Int(modifiedKey), let array = array, let value = array[index] as? T { - if let transformed = transform(value) { - return transformed + var dictionary = self.unboxer.dictionary + var array: [AnyObject]? + var modifiedKey = key + + if isKeyPath && key.containsString(".") { + let components = key.componentsSeparatedByString(".") + for i in 0 ..< components.count { + let keyPathComponent = components[i] + + if i == components.count - 1 { + modifiedKey = keyPathComponent + } else if let nestedDictionary = dictionary[keyPathComponent] as? UnboxableDictionary { + dictionary = nestedDictionary + } else if let nestedArray = dictionary[keyPathComponent] as? [AnyObject] { + array = nestedArray + } else if let array = array, let index = Int(keyPathComponent) where index < array.count, let nestedDictionary = array[index] as? UnboxableDictionary { + dictionary = nestedDictionary + } else { + return nil } } - - return nil - } catch { - return nil } + + if let value = dictionary[modifiedKey] as? T { + if let transformed = transform(value) { + return transformed + } + } else if let index = Int(modifiedKey), let array = array where index < array.count, let value = array[index] as? T { + if let transformed = transform(value) { + return transformed + } + } + + return nil } } @@ -843,6 +859,29 @@ extension UnboxValueResolver where T: CollectionType, T: DictionaryLiteralConver } } +// MARK: - UnboxContainerContext + +private struct UnboxContainerContext { + let key: String + let isKeyPath: Bool +} + +private struct UnboxContainer: UnboxableWithContext { + let model: T + + init(unboxer: Unboxer, context: UnboxContainerContext) { + self.model = unboxer.unbox(context.key, isKeyPath: context.isKeyPath) + } +} + +private struct UnboxArrayContainer: UnboxableWithContext { + let models: [T] + + init(unboxer: Unboxer, context: UnboxContainerContext) { + self.models = unboxer.unbox(context.key, isKeyPath: context.isKeyPath) + } +} + // MARK: - Private extensions private extension Unboxable { @@ -884,68 +923,6 @@ private extension Unboxer { } } - static func unboxer(at key: String, in dictionary: UnboxableDictionary, isKeyPath: Bool, context: Any?) throws -> Unboxer { - let values = try retrieveValues(from: dictionary, at: key, isKeyPath: isKeyPath) - let dictionary = values.dictionary - let array = values.array - let modifiedKey = values.modifiedKey - - if let dictionary = dictionary[modifiedKey] as? UnboxableDictionary { - return Unboxer(dictionary: dictionary, context: context) - } else if let array = array, let index = Int(modifiedKey) where index < array.count , let dictionary = array[index] as? UnboxableDictionary { - print(dictionary) - return Unboxer(dictionary: dictionary, context: context) - } else { - throw UnboxValueError.MissingValueForKey(modifiedKey) - } - } - - static func unboxers(at key: String, in dictionary: UnboxableDictionary, isKeyPath: Bool, context: Any?) throws -> [Unboxer] { - let values = try retrieveValues(from: dictionary, at: key, isKeyPath: isKeyPath) - let dictionary = values.dictionary - let array = values.array - let modifiedKey = values.modifiedKey - - if let dictionaries = dictionary[modifiedKey]?.allValues as? [UnboxableDictionary] { - return dictionaries.map { - return Unboxer(dictionary: $0, context: context) - } - } else if let array = array as? [UnboxableDictionary] { - return array.map { - return Unboxer(dictionary: $0, context: context) - } - } else { - throw UnboxValueError.MissingValueForKey(modifiedKey) - } - } - - static func retrieveValues(from dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool) throws -> (dictionary: UnboxableDictionary, array: [AnyObject]?, modifiedKey: String) { - var dictionary = dictionary - var array: [AnyObject]? - var modifiedKey = key - - if isKeyPath && key.containsString(".") { - let components = key.componentsSeparatedByString(".") - for i in 0 ..< components.count { - let keyPathComponent = components[i] - - if i == components.count - 1 { - modifiedKey = keyPathComponent - } else if let nestedDictionary = dictionary[keyPathComponent] as? UnboxableDictionary { - dictionary = nestedDictionary - } else if let nestedArray = dictionary[keyPathComponent] as? [AnyObject] { - array = nestedArray - } else if let array = array, let index = Int(keyPathComponent) where index < array.count, let nestedDictionary = array[index] as? UnboxableDictionary { - dictionary = nestedDictionary - } else { - throw UnboxValueError.MissingValueForKey(key) - } - } - } - - return (dictionary, array, modifiedKey) - } - func performUnboxing() throws -> T { let unboxed = T(unboxer: self) try self.throwIfFailed() diff --git a/Tests/UnboxTests.swift b/Tests/UnboxTests.swift index d172049..7c894d4 100644 --- a/Tests/UnboxTests.swift +++ b/Tests/UnboxTests.swift @@ -1237,12 +1237,7 @@ class UnboxTests: XCTestCase { let unboxed: UnboxTestSimpleMock = try Unbox(dictionary, at: "B") XCTAssertEqual(unboxed.int, 14) } catch { - switch error { - case UnboxValueError.MissingValueForKey(let missingKey): - XCTAssertEqual(missingKey, "B") - default: - XCTFail("Unexpected error thrown: \(error)") - } + // Test Passed } } @@ -1274,12 +1269,7 @@ class UnboxTests: XCTestCase { let unboxed: UnboxTestSimpleMock = try Unbox(dictionary, at: "A.B", isKeyPath: true) XCTAssertEqual(unboxed.int, 14) } catch { - switch error { - case UnboxValueError.MissingValueForKey(let missingKey): - XCTAssertEqual(missingKey, "B") - default: - XCTFail("Unexpected error thrown: \(error)") - } + // Test Passed } } @@ -1287,13 +1277,13 @@ class UnboxTests: XCTestCase { let dictionary: UnboxableDictionary = [ "A": [ "B": [ - "C": [ + [ "int": 14 ], - "D": [ + [ "int": 14 ], - "E": [ + [ "int": 14 ] ] @@ -1310,38 +1300,6 @@ class UnboxTests: XCTestCase { } } - func testUnboxingArrayStartingAtMissingCustomKeyPath() { - let dictionary: UnboxableDictionary = [ - "A": [ - "B": [ - "C": [ - "int": 14 - ], - "D": [ - "int": 14 - ], - "E": [ - "int": 14 - ] - ] - ] - ] - - do { - let unboxedArray: [UnboxTestSimpleMock] = try Unbox(dictionary, at: "A.C", isKeyPath: true) - unboxedArray.forEach { - XCTAssertEqual($0.int, 14) - } - } catch { - switch error { - case UnboxValueError.MissingValueForKey(let missingKey): - XCTAssertEqual(missingKey, "C") - default: - XCTFail("Unexpected error thrown: \(error)") - } - } - } - func testUnboxingArrayIndexStartingAtCustomKeyPath() { let dictionary: UnboxableDictionary = ["A": ["B": [["int": 14], ["int": 14], ["int": 20]]]] @@ -1362,12 +1320,7 @@ class UnboxTests: XCTestCase { do { try Unbox(dictionary, at: "A.B.3", isKeyPath: true) as UnboxTestSimpleMock } catch { - switch error { - case UnboxValueError.MissingValueForKey(let missingKey): - XCTAssertEqual(missingKey, "3") - default: - XCTFail("Unexpected error thrown: \(error)") - } + // Test Passed } }