Skip to content

Commit

Permalink
Added support for directly unboxing a model using keyPath
Browse files Browse the repository at this point in the history
  • Loading branch information
clayellis committed Jul 25, 2016
1 parent 36fcaab commit acd997f
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 172 deletions.
66 changes: 33 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
```

Expand Down Expand Up @@ -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")
}
}
```

Expand Down
149 changes: 63 additions & 86 deletions Sources/Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ public func Unbox<T: Unboxable>(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<T: Unboxable>(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<T: Unboxable>(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true) throws -> T {
let context = UnboxContainerContext(key: key, isKeyPath: isKeyPath)
let container: UnboxContainer<T> = 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<T: Unboxable>(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<T: Unboxable>(dictionary: UnboxableDictionary, at key: String, isKeyPath: Bool = true) throws -> [T] {
let context = UnboxContainerContext(key: key, isKeyPath: isKeyPath)
let container: UnboxArrayContainer<T> = 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`.
Expand Down Expand Up @@ -786,26 +788,40 @@ private class UnboxValueResolver<T> {
}

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

Expand Down Expand Up @@ -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<T: Unboxable>: UnboxableWithContext {
let model: T

init(unboxer: Unboxer, context: UnboxContainerContext) {
self.model = unboxer.unbox(context.key, isKeyPath: context.isKeyPath)
}
}

private struct UnboxArrayContainer<T: Unboxable>: UnboxableWithContext {
let models: [T]

init(unboxer: Unboxer, context: UnboxContainerContext) {
self.models = unboxer.unbox(context.key, isKeyPath: context.isKeyPath)
}
}

// MARK: - Private extensions

private extension Unboxable {
Expand Down Expand Up @@ -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<T: Unboxable>() throws -> T {
let unboxed = T(unboxer: self)
try self.throwIfFailed()
Expand Down
59 changes: 6 additions & 53 deletions Tests/UnboxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -1274,26 +1269,21 @@ 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
}
}

func testUnboxingArrayStartingAtCustomKeyPath() {
let dictionary: UnboxableDictionary = [
"A": [
"B": [
"C": [
[
"int": 14
],
"D": [
[
"int": 14
],
"E": [
[
"int": 14
]
]
Expand All @@ -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]]]]
Expand All @@ -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
}
}

Expand Down

0 comments on commit acd997f

Please sign in to comment.