Skip to content

Commit

Permalink
Added keyPath support where the result is an array of Unboxables
Browse files Browse the repository at this point in the history
Added support for keyPaths which contain array indices
Added tests for both new features
  • Loading branch information
clayellis committed Jul 20, 2016
1 parent 57ed1fb commit 193f4cf
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 38 deletions.
107 changes: 69 additions & 38 deletions Sources/Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public func Unbox<T: Unboxable>(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<T: Unboxable>(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<T: Unboxable>(dictionaries: [UnboxableDictionary], context: Any? = nil, allowInvalidElements: Bool = false) throws -> [T] {
return try dictionaries.mapAllowingInvalidElements(allowInvalidElements, transform: {
Expand Down Expand Up @@ -779,40 +786,26 @@ private class UnboxValueResolver<T> {
}

func resolveOptionalValueForKey<R>(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
}
}

Expand Down Expand Up @@ -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(".") {
Expand All @@ -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<T: Unboxable>() throws -> T {
Expand Down
88 changes: 88 additions & 0 deletions Tests/UnboxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 193f4cf

Please sign in to comment.