Skip to content

Commit

Permalink
Merge pull request JohnSundell#92 from clayellis/master
Browse files Browse the repository at this point in the history
Fix Issue #73, Begin Unboxing at key
  • Loading branch information
JohnSundell authored Jul 25, 2016
2 parents be5fd07 + 0a0b49e commit ca8ae1d
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 13 deletions.
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,14 @@ You can also use key paths (for both dictionary keys and array indexes) to unbox
```

```swift
struct User: Unboxable {
struct User {
let name: String
let age: Int
let runningDistance: Int
let primaryDeviceName: String
}

extension User: Unboxable {
init(unboxer: Unboxer) {
self.name = unboxer.unbox("name")
self.age = unboxer.unbox("age")
Expand All @@ -264,6 +266,56 @@ struct User: Unboxable {
}
```

You can also use key paths to directly unbox nested JSON structures. This is useful when you only need to extract a specific object (or objects) out of the JSON body.

```json
{
"company": {
"name": "Spotify",
},
"jobOpenings": [
{
"title": "Swift Developer",
"salary": 120000
},
{
"title": "UI Designer",
"salary": 100000
},
]
}
```

```swift
struct JobOpening {
let title: String
let salary: Int
}

extension JobOpening: Unboxable {
init(unboxer: Unboxer) {
self.title = unboxer.unbox("title")
self.salary = unboxer.unbox("salary")
}
}

struct Company {
let name: String
}

extension Company: Unboxable {
init(unboxer: Unboxer) {
self.name = unboxer.unbox("name")
}
}
```

```swift
let company: Company = try Unbox(json, at: "company")
let jobOpenings: [JobOpening] = try Unbox(json, at: "jobOpenings")
let featuredOpening: JobOpening = try Unbox(json, at: "jobOpenings.0")
```

### Custom unboxing

Sometimes you need more fine grained control over the decoding process, and even though Unbox was designed for simplicity, it also features a powerful custom unboxing API that enables you to take control of how an object gets unboxed. This comes very much in handy when using Unbox together with Core Data, when using dependency injection, or when aggregating data from multiple sources. Here's an example:
Expand Down
61 changes: 49 additions & 12 deletions Sources/Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,35 @@ public func Unbox<T: Unboxable>(dictionary: UnboxableDictionary, context: Any? =
return try Unboxer(dictionary: dictionary, context: context).performUnboxing()
}

/// 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) throws -> T {
let context = UnboxContainerContext(key: key, isKeyPath: isKeyPath)
let container: UnboxContainer<T> = try Unbox(dictionary, context: context)
return container.model
}

/// 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: {
try Unbox($0, context: context)
})
}

/// 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 binary data into a model `T`, optionally using a contextual object. Throws `UnboxError`.
public func Unbox<T: Unboxable>(data: NSData, context: Any? = nil) throws -> T {
return try Unboxer.unboxerFromData(data, context: context).performUnboxing()
return try Unboxer.unboxer(from: data, context: context).performUnboxing()
}

/// Unbox binary data into an array of `T`, optionally using a contextual object and/or invalid elements. Throws `UnboxError`.
public func Unbox<T: Unboxable>(data: NSData, context: Any? = nil, allowInvalidElements: Bool = false) throws -> [T] {
return try Unboxer.unboxersFromData(data, context: context).mapAllowingInvalidElements(allowInvalidElements, transform: {
return try Unboxer.unboxers(from: data, context: context).mapAllowingInvalidElements(allowInvalidElements, transform: {
return try $0.performUnboxing()
})
}
Expand All @@ -73,12 +87,12 @@ public func Unbox<T: UnboxableWithContext>(dictionaries: [UnboxableDictionary],

/// Unbox binary data into a model `T` using a required contextual object. Throws `UnboxError`.
public func Unbox<T: UnboxableWithContext>(data: NSData, context: T.ContextType) throws -> T {
return try Unboxer.unboxerFromData(data, context: context).performUnboxingWithContext(context)
return try Unboxer.unboxer(from: data, context: context).performUnboxingWithContext(context)
}

/// Unbox binary data into an array of `T` using a required contextual object and/or invalid elements. Throws `UnboxError`.
public func Unbox<T: UnboxableWithContext>(data: NSData, context: T.ContextType, allowInvalidElements: Bool = false) throws -> [T] {
return try Unboxer.unboxersFromData(data, context: context).mapAllowingInvalidElements(allowInvalidElements, transform: {
return try Unboxer.unboxers(from: data, context: context).mapAllowingInvalidElements(allowInvalidElements, transform: {
return try $0.performUnboxingWithContext(context)
})
}
Expand Down Expand Up @@ -423,7 +437,7 @@ public class Unboxer {

/// Perform custom unboxing using an Unboxer (created from NSData) passed to a closure, or throw an UnboxError
public static func performCustomUnboxingWithData<T>(data: NSData, context: Any? = nil, closure: Unboxer throws -> T?) throws -> T {
return try Unboxer.unboxerFromData(data, context: context).performCustomUnboxingWithClosure(closure)
return try Unboxer.unboxer(from: data, context: context).performCustomUnboxingWithClosure(closure)
}

// MARK: - Value accessing API
Expand Down Expand Up @@ -787,7 +801,7 @@ private class UnboxValueResolver<T> {
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 {
Expand All @@ -806,12 +820,12 @@ private class UnboxValueResolver<T> {
}
}
}

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 {
} 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
}
Expand Down Expand Up @@ -855,6 +869,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 All @@ -870,7 +907,7 @@ private extension UnboxableWithContext {
}

private extension Unboxer {
static func unboxerFromData(data: NSData, context: Any?) throws -> Unboxer {
static func unboxer(from data: NSData, context: Any?) throws -> Unboxer {
do {
guard let dictionary = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? UnboxableDictionary else {
throw UnboxError.InvalidData
Expand All @@ -882,15 +919,15 @@ private extension Unboxer {
}
}

static func unboxersFromData(data: NSData, context: Any?) throws -> [Unboxer] {
static func unboxers(from data: NSData, context: Any?) throws -> [Unboxer] {
do {
guard let array = try NSJSONSerialization.JSONObjectWithData(data, options: [.AllowFragments]) as? [UnboxableDictionary] else {
throw UnboxError.InvalidData
}

return array.map({
return array.map {
return Unboxer(dictionary: $0, context: context)
})
}
} catch {
throw UnboxError.InvalidData
}
Expand Down
113 changes: 113 additions & 0 deletions Tests/UnboxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,120 @@ class UnboxTests: XCTestCase {
XCTFail("Unexpected error thrown: \(error)")
}
}

func testUnboxingStartingAtCustomKey() {
let dictionary: UnboxableDictionary = [
"A": [
"int": 14
]
]

do {
let unboxed: UnboxTestSimpleMock = try Unbox(dictionary, at: "A")
XCTAssertEqual(unboxed.int, 14)
} catch {
XCTFail("Unexpected error thrown: \(error)")
}
}

func testUnboxingStartingAtMissingCustomKey() {
let dictionary: UnboxableDictionary = [
"A": [
"int": 14
]
]

do {
let _ : UnboxTestSimpleMock = try Unbox(dictionary, at: "B")
XCTFail()
} catch {
// Test Passed
}
}

func testUnboxingStartingAtCustomKeyPath() {
let dictionary: UnboxableDictionary = [
"A": [
"B": [
"int": 14
]
]
]

do {
let unboxed: UnboxTestSimpleMock = try Unbox(dictionary, at: "A.B", isKeyPath: true)
XCTAssertEqual(unboxed.int, 14)
} catch {
XCTFail("Unexpected error thrown: \(error)")
}
}

func testUnboxingStartingAtMissingCustomKeyPath() {
let dictionary: UnboxableDictionary = [
"A": [
"int": 14
]
]

do {
let _: UnboxTestSimpleMock = try Unbox(dictionary, at: "A.B", isKeyPath: true)
XCTFail()
} catch {
// Test Passed
}
}

func testUnboxingArrayStartingAtCustomKeyPath() {
let dictionary: UnboxableDictionary = [
"A": [
"B": [
[
"int": 14
],
[
"int": 14
],
[
"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 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 {
// Test Passed
}
}

}

private func UnboxTestDictionaryWithAllRequiredKeysWithValidValues(nested: Bool) -> UnboxableDictionary {
Expand Down

0 comments on commit ca8ae1d

Please sign in to comment.