From 193f4cf83bdfa5b2c7e0b51a6ced4d72a5c948fd Mon Sep 17 00:00:00 2001 From: Clay Ellis Date: Wed, 20 Jul 2016 14:57:21 -0600 Subject: [PATCH] Added keyPath support where the result is an array of Unboxables Added support for keyPaths which contain array indices Added tests for both new features --- Sources/Unbox.swift | 107 ++++++++++++++++++++++++++--------------- Tests/UnboxTests.swift | 88 +++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 38 deletions(-) diff --git a/Sources/Unbox.swift b/Sources/Unbox.swift index cf9f680..1f96fc5 100644 --- a/Sources/Unbox.swift +++ b/Sources/Unbox.swift @@ -45,6 +45,13 @@ public func Unbox(dictionary: UnboxableDictionary, at key: String, return try Unboxer.unboxer(at: key, in: dictionary, isKeyPath: isKeyPath, context: context).performUnboxing() } +/// 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 = false, 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 of JSON dictionaries into an array of `T`, optionally using a contextual object and/or invalid elements. Throws `UnboxError`. public func Unbox(dictionaries: [UnboxableDictionary], context: Any? = nil, allowInvalidElements: Bool = false) throws -> [T] { return try dictionaries.mapAllowingInvalidElements(allowInvalidElements, transform: { @@ -779,40 +786,26 @@ private class UnboxValueResolver { } func resolveOptionalValueForKey(key: String, isKeyPath: Bool, transform: T -> R?) -> R? { - 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 + 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 } } + + 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, let value = array[index] as? T { - if let transformed = transform(value) { - return transformed - } - } - - return nil } } @@ -883,16 +876,52 @@ private extension Unboxer { throw UnboxError.InvalidData } - return array.map({ + return array.map { return Unboxer(dictionary: $0, context: context) - }) + } } catch { throw UnboxError.InvalidData } } 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(".") { @@ -904,15 +933,17 @@ private extension Unboxer { 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) } } } - guard let nestedDictionary = dictionary[modifiedKey] as? UnboxableDictionary else { - throw UnboxValueError.MissingValueForKey(modifiedKey) - } - - return Unboxer(dictionary: nestedDictionary, context: context) + return (dictionary, array, modifiedKey) } func performUnboxing() throws -> T { diff --git a/Tests/UnboxTests.swift b/Tests/UnboxTests.swift index b1157cf..d172049 100644 --- a/Tests/UnboxTests.swift +++ b/Tests/UnboxTests.swift @@ -1283,6 +1283,94 @@ class UnboxTests: XCTestCase { } } + func testUnboxingArrayStartingAtCustomKeyPath() { + let dictionary: UnboxableDictionary = [ + "A": [ + "B": [ + "C": [ + "int": 14 + ], + "D": [ + "int": 14 + ], + "E": [ + "int": 14 + ] + ] + ] + ] + + do { + let unboxedArray: [UnboxTestSimpleMock] = try Unbox(dictionary, at: "A.B", isKeyPath: true) + unboxedArray.forEach { + XCTAssertEqual($0.int, 14) + } + } catch { + XCTFail("Unexpected error thrown: \(error)") + } + } + + 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]]]] + + do { + let unboxed: UnboxTestSimpleMock = try Unbox(dictionary, at: "A.B.2", isKeyPath: true) + XCTAssertEqual(unboxed.int, 20) + + } catch { + XCTFail("Unexpected error thrown: \(error)") + } + } + + func testUnboxingArrayInvalidIndexStartingAtCustomKeyPath() { + let dictionary: UnboxableDictionary = + ["A": ["B": [["int": 14], ["int": 14], ["int": 20]]]] + + 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)") + } + } + } + } private func UnboxTestDictionaryWithAllRequiredKeysWithValidValues(nested: Bool) -> UnboxableDictionary {