diff --git a/JSONCodable.playground/Contents.swift b/JSONCodable.playground/Contents.swift index 10e14c7..4ef77b1 100644 --- a/JSONCodable.playground/Contents.swift +++ b/JSONCodable.playground/Contents.swift @@ -31,14 +31,14 @@ We'll add conformance to `JSONEncodable`. You may also add conformance to `JSONC */ extension User: JSONEncodable { - func toJSON() throws -> AnyObject { - return try JSONEncoder.create({ (encoder) -> Void in + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { + return try JSONEncoder.create(options) { (encoder) -> Void in try encoder.encode(id, key: "id") try encoder.encode(name, key: "full_name") try encoder.encode(email, key: "email") try encoder.encode(company, key: "company") try encoder.encode(friends, key: "friends") - }) + } } } @@ -54,31 +54,21 @@ We'll add conformance to `JSONDecodable`. You may also add conformance to `JSONC */ extension User: JSONDecodable { - init?(JSONDictionary: JSONObject) { - let decoder = JSONDecoder(object: JSONDictionary) - do { - id = try decoder.decode("id") - name = try decoder.decode("full_name") - email = try decoder.decode("email") - company = try decoder.decode("company") - friends = try decoder.decode("friends") - } - catch { - return nil - } + init(object: JSONObject) throws { + let decoder = JSONDecoder(object: object) + id = try decoder.decode("id") + name = try decoder.decode("full_name") + email = try decoder.decode("email") + company = try decoder.decode("company") + friends = try decoder.decode("friends") } } extension Company: JSONDecodable { - init?(JSONDictionary: JSONObject) { - let decoder = JSONDecoder(object: JSONDictionary) - do { - name = try decoder.decode("name") - address = try decoder.decode("address") - } - catch { - return nil - } + init(object: JSONObject) throws { + let decoder = JSONDecoder(object: object) + name = try decoder.decode("name") + address = try decoder.decode("address") } } @@ -115,7 +105,7 @@ We can instantiate `User` using one of provided initializers: - `init?(JSONString: String)` */ -let user = User(JSONDictionary: JSON)! +var user = try! User(object: JSON) print("Decoded: \n\(user)\n\n") @@ -126,7 +116,7 @@ And encode it to JSON using one of the provided methods: */ do { - let dict = try user.toJSON() + let dict = try user.toJSON([]) print("Encoded: \n\(dict as! JSONObject)\n\n") } catch { diff --git a/JSONCodable.playground/contents.xcplayground b/JSONCodable.playground/contents.xcplayground index 0b96219..3de2b51 100644 --- a/JSONCodable.playground/contents.xcplayground +++ b/JSONCodable.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/JSONCodable.playground/timeline.xctimeline b/JSONCodable.playground/timeline.xctimeline index bf468af..46c39b3 100644 --- a/JSONCodable.playground/timeline.xctimeline +++ b/JSONCodable.playground/timeline.xctimeline @@ -2,5 +2,10 @@ + + diff --git a/JSONCodable/JSONCodable.swift b/JSONCodable/JSONCodable.swift index e62a58a..d0b936a 100644 --- a/JSONCodable/JSONCodable.swift +++ b/JSONCodable/JSONCodable.swift @@ -19,9 +19,10 @@ extension Double: JSONCompatible {} extension Float: JSONCompatible {} extension Bool: JSONCompatible {} extension Int: JSONCompatible {} +extension NSNull: JSONCompatible {} extension JSONCompatible { - public func toJSON() throws -> AnyObject { + public func toJSON(options: JSONEncodingOptions) throws -> AnyObject { return self as! AnyObject } } diff --git a/JSONCodable/JSONEncodable.swift b/JSONCodable/JSONEncodable.swift index 543f9f7..d3cb3db 100644 --- a/JSONCodable/JSONEncodable.swift +++ b/JSONCodable/JSONEncodable.swift @@ -42,21 +42,28 @@ public enum JSONEncodableError: ErrorType, CustomStringConvertible { } } -// Struct -> Dictionary +public struct JSONEncodingOptions: OptionSetType { + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue; + } + public static let EncodeNulls = JSONEncodingOptions(rawValue: 1 << 0) +} public protocol JSONEncodable { - func toJSON() throws -> AnyObject + func toJSON(options: JSONEncodingOptions) throws -> AnyObject } public extension JSONEncodable { - func toJSON() throws -> AnyObject { + + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { let mirror = Mirror(reflecting: self) guard let style = mirror.displayStyle where style == .Struct || style == .Class else { throw JSONEncodableError.IncompatibleTypeError(elementType: self.dynamicType) } - return try JSONEncoder.create({ (encoder) -> Void in + return try JSONEncoder.create(options) { (encoder) -> Void in // loop through all properties (instance variables) for (labelMaybe, valueMaybe) in mirror.children { guard let label = labelMaybe else { @@ -87,18 +94,18 @@ public extension JSONEncodable { throw JSONEncodableError.ChildIncompatibleTypeError(key: label, elementType: value.dynamicType) } } - }) + } } } public extension Array {//where Element: JSONEncodable { private var wrapped: [Any] { return self.map{$0} } - public func toJSON() throws -> AnyObject { + public func toJSON(options: JSONEncodingOptions) throws -> AnyObject { var results: [AnyObject] = [] for item in self.wrapped { if let item = item as? JSONEncodable { - results.append(try item.toJSON()) + results.append(try item.toJSON(options)) } else { throw JSONEncodableError.ArrayIncompatibleTypeError(elementType: item.dynamicType) @@ -111,11 +118,11 @@ public extension Array {//where Element: JSONEncodable { // Dictionary convenience methods public extension Dictionary {//where Key: String, Value: JSONEncodable { - public func toJSON() throws -> AnyObject { + public func toJSON(options: JSONEncodingOptions) throws -> AnyObject { var result: [String: AnyObject] = [:] for (k, item) in self { if let item = item as? JSONEncodable { - result[String(k)] = try item.toJSON() + result[String(k)] = try item.toJSON(options) } else { throw JSONEncodableError.DictionaryIncompatibleTypeError(elementType: item.dynamicType) @@ -129,9 +136,11 @@ public extension Dictionary {//where Key: String, Value: JSONEncodable { public class JSONEncoder { var object = JSONObject() + public var options: JSONEncodingOptions = [] - public static func create(@noescape setup: (encoder: JSONEncoder) throws -> Void) rethrows -> JSONObject { + public static func create(options: JSONEncodingOptions, @noescape setup: (encoder: JSONEncoder) throws -> Void) rethrows -> JSONObject { let encoder = JSONEncoder() + encoder.options = options try setup(encoder: encoder) return encoder.object } @@ -145,21 +154,18 @@ public class JSONEncoder { */ // JSONEncodable - public func encode(value: Encodable, key: String) throws { - let result = try value.toJSON() - object[key] = result - } - private func encode(value: JSONEncodable, key: String) throws { - let result = try value.toJSON() + public func encode(value: JSONEncodable, key: String) throws { + let result = try value.toJSON(options) object[key] = result } // JSONEncodable? public func encode(value: Encodable?, key: String) throws { guard let actual = value else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } - let result = try actual.toJSON() + let result = try actual.toJSON(options) object[key] = result } @@ -168,65 +174,55 @@ public class JSONEncoder { guard let compatible = value.rawValue as? JSONCompatible else { return } - let result = try compatible.toJSON() + let result = try compatible.toJSON(options) object[key] = result } // Enum? public func encode(value: Enum?, key: String) throws { guard let actual = value else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } guard let compatible = actual.rawValue as? JSONCompatible else { return } - let result = try compatible.toJSON() + let result = try compatible.toJSON(options) object[key] = result } // [JSONEncodable] public func encode(array: [Encodable], key: String) throws { - guard array.count > 0 else { - return - } - let result = try array.toJSON() + let result = try array.toJSON(options) object[key] = result } public func encode(array: [JSONEncodable], key: String) throws { - guard array.count > 0 else { - return - } - let result = try array.toJSON() + let result = try array.toJSON(options) object[key] = result } - private func encode(array: JSONArray, key: String) throws { - guard array.count > 0 && array.elementsAreJSONEncodable() else { + func encode(array: JSONArray, key: String) throws { + guard array.elementsAreJSONEncodable() else { return } let encodable = array.elementsMadeJSONEncodable() - let result = try encodable.toJSON() + let result = try encodable.toJSON(options) object[key] = result } // [JSONEncodable]? public func encode(value: [Encodable]?, key: String) throws { guard let actual = value else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } - guard actual.count > 0 else { - return - } - let result = try actual.toJSON() + let result = try actual.toJSON(options) object[key] = result } // [Enum] public func encode(value: [Enum], key: String) throws { - guard value.count > 0 else { - return - } let result = try value.flatMap { - try ($0.rawValue as? JSONCompatible)?.toJSON() + try ($0.rawValue as? JSONCompatible)?.toJSON(options) } object[key] = result } @@ -234,50 +230,40 @@ public class JSONEncoder { // [Enum]? public func encode(value: [Enum]?, key: String) throws { guard let actual = value else { - return - } - guard actual.count > 0 else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } let result = try actual.flatMap { - try ($0.rawValue as? JSONCompatible)?.toJSON() + try ($0.rawValue as? JSONCompatible)?.toJSON(options) } object[key] = result } // [String:JSONEncodable] public func encode(dictionary: [String:Encodable], key: String) throws { - guard dictionary.count > 0 else { - return - } - let result = try dictionary.toJSON() + let result = try dictionary.toJSON(options) object[key] = result } public func encode(dictionary: [String:JSONEncodable], key: String) throws { - guard dictionary.count > 0 else { - return - } - let result = try dictionary.toJSON() + let result = try dictionary.toJSON(options) object[key] = result } - private func encode(dictionary: JSONDictionary, key: String) throws { - guard dictionary.count > 0 && dictionary.valuesAreJSONEncodable() else { + func encode(dictionary: JSONDictionary, key: String) throws { + guard dictionary.valuesAreJSONEncodable() else { return } let encodable = dictionary.valuesMadeJSONEncodable() - let result = try encodable.toJSON() + let result = try encodable.toJSON(options) object[key] = result } // [String:JSONEncodable]? public func encode(value: [String:Encodable]?, key: String) throws { guard let actual = value else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } - guard actual.count > 0 else { - return - } - let result = try actual.toJSON() + let result = try actual.toJSON(options) object[key] = result } @@ -292,6 +278,7 @@ public class JSONEncoder { // JSONTransformable? public func encode(value: DecodedType?, key: String, transformer: JSONTransformer) throws { guard let actual = value else { + if options.contains(.EncodeNulls) { object[key] = NSNull() } return } guard let result = transformer.encoding(actual) else { diff --git a/JSONCodable/JSONString.swift b/JSONCodable/JSONString.swift index 74af215..da4ec37 100644 --- a/JSONCodable/JSONString.swift +++ b/JSONCodable/JSONString.swift @@ -16,7 +16,7 @@ public extension JSONEncodable { case is Bool, is Int, is Float, is Double: return String(self) default: - let json = try toJSON() + let json = try toJSON([]) let data = try NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions(rawValue: 0)) guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else { return "" diff --git a/JSONCodableTests/EnumTests.swift b/JSONCodableTests/EnumTests.swift index f8d0f4d..8395135 100644 --- a/JSONCodableTests/EnumTests.swift +++ b/JSONCodableTests/EnumTests.swift @@ -33,7 +33,7 @@ class EnumTests: XCTestCase { } func testEncodingEnum() { - guard let json = try? decodedValue.toJSON() else { + guard let json = try? decodedValue.toJSON([]) else { XCTFail() return } @@ -45,7 +45,7 @@ class EnumTests: XCTestCase { XCTAssertEqual(castedJSON, encodedValue) - guard let json2 = try? decodedValue2.toJSON() else { + guard let json2 = try? decodedValue2.toJSON([]) else { XCTFail() return } diff --git a/JSONCodableTests/Food.swift b/JSONCodableTests/Food.swift index 14847d1..1f80136 100644 --- a/JSONCodableTests/Food.swift +++ b/JSONCodableTests/Food.swift @@ -38,10 +38,10 @@ extension Food: JSONCodable { cuisines = try decoder.decode("cuisines") } - func toJSON() throws -> AnyObject { - return try JSONEncoder.create({ (encoder) -> Void in + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { + return try JSONEncoder.create(options) { (encoder) -> Void in try encoder.encode(name, key: "name") try encoder.encode(cuisines, key: "cuisines") - }) + } } } \ No newline at end of file diff --git a/JSONCodableTests/Fruit.swift b/JSONCodableTests/Fruit.swift index 3d981bc..1450acb 100644 --- a/JSONCodableTests/Fruit.swift +++ b/JSONCodableTests/Fruit.swift @@ -30,10 +30,13 @@ extension Fruit: JSONCodable { color = try decoder.decode("color") } - func toJSON() throws -> AnyObject { - return try JSONEncoder.create({ (encoder) -> Void in + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { + return try JSONEncoder.create(options) { (encoder) -> Void in + NSLog("a") try encoder.encode(name, key: "name") + NSLog("b") try encoder.encode(color, key: "color") - }) + NSLog("c") + } } } \ No newline at end of file diff --git a/JSONCodableTests/HelperTests.swift b/JSONCodableTests/HelperTests.swift index 0af0c17..3ecc816 100644 --- a/JSONCodableTests/HelperTests.swift +++ b/JSONCodableTests/HelperTests.swift @@ -24,10 +24,10 @@ class HelperTests: XCTestCase { let notEncodableArray:[NotEncodable] = [NotEncodable()] XCTAssert(!notEncodableArray.elementsAreJSONEncodable(), "Array of type [NotEncodable] should not be encodable") - let _ = try? JSONEncoder.create({ (encoder) -> Void in + let _ = try? JSONEncoder.create([]) { (encoder) -> Void in try encoder.encode(intArray, key: "intArray") try encoder.encode(encodableArray, key: "encodableArray") - }) + } } @@ -41,10 +41,10 @@ class HelperTests: XCTestCase { let notEncodableDict:[String:NotEncodable] = ["a":NotEncodable()] XCTAssert(!notEncodableDict.valuesAreJSONEncodable(), "Dictionary of type [String:NotEncodable] should not be encodable") - let _ = try? JSONEncoder.create({ (encoder) -> Void in + let _ = try? JSONEncoder.create([]) { (encoder) -> Void in try encoder.encode(intDict, key: "intArray") try encoder.encode(encodableDict, key: "encodableArray") - }) + } } } diff --git a/JSONCodableTests/ImageAsset.swift b/JSONCodableTests/ImageAsset.swift index 5561b6c..28e30f0 100644 --- a/JSONCodableTests/ImageAsset.swift +++ b/JSONCodableTests/ImageAsset.swift @@ -19,11 +19,11 @@ func ==(lhs: ImageAsset, rhs: ImageAsset) -> Bool { } extension ImageAsset: JSONEncodable { - func toJSON() throws -> AnyObject { - return try JSONEncoder.create({ (encoder) -> Void in + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { + return try JSONEncoder.create(options) { (encoder) -> Void in try encoder.encode(name, key: "name") try encoder.encode(uri, key: "uri", transformer: JSONTransformers.StringToNSURL) - }) + } } } diff --git a/JSONCodableTests/RegularTests.swift b/JSONCodableTests/RegularTests.swift index 270bbfc..8b3cc43 100644 --- a/JSONCodableTests/RegularTests.swift +++ b/JSONCodableTests/RegularTests.swift @@ -19,10 +19,25 @@ class RegularTests: XCTestCase { "address": "1 Infinite Loop, Cupertino, CA" ], "friends": [ - ["id": 27, "full_name": "Bob Jefferson"], - ["id": 29, "full_name": "Jen Jackson"] + ["id": 27, "full_name": "Bob Jefferson", "friends": []], + ["id": 29, "full_name": "Jen Jackson", "friends": []] ], - "friendsLookup": ["Bob Jefferson": ["id": 27, "full_name": "Bob Jefferson"]] + "friendsLookup": ["Bob Jefferson": ["id": 27, "full_name": "Bob Jefferson", "friends": []]] + ] + + let encodedValueWithNulls = [ + "id": 24, + "full_name": "John Appleseed", + "email": "john@appleseed.com", + "company": [ + "name": "Apple", + "address": "1 Infinite Loop, Cupertino, CA" + ], + "friends": [ + ["id": 27, "full_name": "Bob Jefferson", "email": NSNull(), "company": NSNull(), "friends": [], "friendsLookup" : NSNull()], + ["id": 29, "full_name": "Jen Jackson", "email": NSNull(), "company": NSNull(), "friends": [], "friendsLookup" : NSNull()] + ], + "friendsLookup": ["Bob Jefferson": ["id": 27, "full_name": "Bob Jefferson", "email": NSNull(), "company": NSNull(), "friends": [], "friendsLookup" : NSNull()]] ] let decodedValue = User( id: 24, @@ -46,11 +61,29 @@ class RegularTests: XCTestCase { } func testEncodingRegular() { - guard let json = try? decodedValue.toJSON() else { + guard let json = try? decodedValue.toJSON([]) else { XCTFail() return } XCTAssertEqual(json as! [String : NSObject], encodedValue) } + + func testNullEncoding() { + guard let json = try? decodedValue.toJSON([.EncodeNulls]) else { + XCTFail() + return + } + + XCTAssertEqual(json as! [String : NSObject], encodedValueWithNulls) + } + + func testNullDecoding() { + guard let user = try? User(object: encodedValue) else { + XCTFail() + return + } + + XCTAssertEqual(user, decodedValue) + } } diff --git a/JSONCodableTests/TransformerTests.swift b/JSONCodableTests/TransformerTests.swift index f323923..020b1a3 100644 --- a/JSONCodableTests/TransformerTests.swift +++ b/JSONCodableTests/TransformerTests.swift @@ -29,7 +29,7 @@ class TransformerTests: XCTestCase { } func testEncodingTransformer() { - guard let json = try? decodedValue.toJSON() else { + guard let json = try? decodedValue.toJSON([]) else { XCTFail() return } diff --git a/JSONCodableTests/User.swift b/JSONCodableTests/User.swift index a4fac6d..b0dae3e 100644 --- a/JSONCodableTests/User.swift +++ b/JSONCodableTests/User.swift @@ -27,15 +27,15 @@ func ==(lhs: User, rhs: User) -> Bool { } extension User: JSONEncodable { - func toJSON() throws -> AnyObject { - return try JSONEncoder.create({ (encoder) -> Void in + func toJSON(options: JSONEncodingOptions) throws -> AnyObject { + return try JSONEncoder.create(options) { (encoder) -> Void in try encoder.encode(id, key: "id") try encoder.encode(name, key: "full_name") try encoder.encode(email, key: "email") try encoder.encode(company, key: "company") try encoder.encode(friends, key: "friends") try encoder.encode(friendsLookup, key: "friendsLookup") - }) + } } } @@ -49,4 +49,4 @@ extension User: JSONDecodable { friends = try decoder.decode("friends") friendsLookup = try decoder.decode("friendsLookup") } -} \ No newline at end of file +}