diff --git a/Demo/Demo/User.swift b/Demo/Demo/User.swift index d8c6621..2350c72 100644 --- a/Demo/Demo/User.swift +++ b/Demo/Demo/User.swift @@ -16,6 +16,7 @@ struct User { var company: Company? var friends: [User] = [] var website: NSURL? + var props: [String: String] = [:] } extension User: JSONCodable { @@ -27,6 +28,7 @@ extension User: JSONCodable { company = try JSONDictionary.decode("company") friends = try JSONDictionary.decode("friends") website = try JSONDictionary.decode("website.url", transformer: JSONTransformers.StringToNSURL) + props = try JSONDictionary.decode("props") } catch { print(error) @@ -42,6 +44,7 @@ extension User: JSONCodable { try result.encode(company, key: "company") try result.encode(friends, key: "friends") try result.encode(website, key: "website", transformer: JSONTransformers.StringToNSURL) + try result.encode(props, key: "props") return result } } diff --git a/Demo/Demo/ViewController.swift b/Demo/Demo/ViewController.swift index c84bdd1..884610b 100644 --- a/Demo/Demo/ViewController.swift +++ b/Demo/Demo/ViewController.swift @@ -27,7 +27,8 @@ class ViewController: UIViewController { ["id": 27, "full_name": "Bob Jefferson"], ["id": 29, "full_name": "Jen Jackson"] ], - "website": ["url": "http://johnappleseed.com"] + "website": ["url": "http://johnappleseed.com"], + "props": ["prop a": "value a", "prop b": "value b"] ] print("Initial JSON:\n\(JSON)\n\n") diff --git a/JSONCodable/JSONDecodable.swift b/JSONCodable/JSONDecodable.swift index 5c451d1..5699fd9 100644 --- a/JSONCodable/JSONDecodable.swift +++ b/JSONCodable/JSONDecodable.swift @@ -82,6 +82,28 @@ public extension Dictionary where Value: AnyObject { } } + // optional array of scalars + public func decode(key: Key) throws -> [String: Value]? { + if let y = get(key) ?? self[key] { + guard let x = y as? [String: Value] else { + throw JSONDecodableError.IncompatibleTypeError(key: key as! String, elementType: y.dynamicType, expectedType: [String: Value].self) + } + return x + } + return nil + } + + // required dictionary of scalars + public func decode(key: Key) throws -> [String: Value] { + guard let y = get(key) ?? self[key] else { + throw JSONDecodableError.MissingTypeError(key: key as! String) + } + guard let x = y as? [String: Value] else { + throw JSONDecodableError.IncompatibleTypeError(key: key as! String, elementType: y.dynamicType, expectedType: [String: Value].self) + } + return x + } + // TODO: validate array elements // optional array of decodables public func decode(key: Key) throws -> [Element]? { @@ -130,6 +152,9 @@ public extension Dictionary where Value: AnyObject { // optional decodable public func decode(key: Key) throws -> Type? { if let y = get(key) ?? self[key] { + if y is NSNull { + return nil + } guard let x = y as? [String : AnyObject] else { throw JSONDecodableError.DictionaryTypeExpectedError(key: key as! String, elementType: y.dynamicType) } diff --git a/JSONCodable/JSONEncodable.swift b/JSONCodable/JSONEncodable.swift index 401b216..9f8e1e7 100644 --- a/JSONCodable/JSONEncodable.swift +++ b/JSONCodable/JSONEncodable.swift @@ -11,6 +11,7 @@ public enum JSONEncodableError: ErrorType, CustomStringConvertible { case IncompatibleTypeError(elementType: Any.Type) case ArrayIncompatibleTypeError(elementType: Any.Type) + case DictionaryIncompatibleTypeError(elementType: Any.Type) case ChildIncompatibleTypeError(key: String, elementType: Any.Type) case TransformerFailedError( key: String @@ -22,6 +23,8 @@ public enum JSONEncodableError: ErrorType, CustomStringConvertible { return "JSONEncodableError: Incompatible type \(elementType)" case let .ArrayIncompatibleTypeError(elementType: elementType): return "JSONEncodableError: Got an array of incompatible type \(elementType)" + case let .DictionaryIncompatibleTypeError(elementType: elementType): + return "JSONEncodableError: Got an dictionary of incompatible type \(elementType)" case let .ChildIncompatibleTypeError(key: key, elementType: elementType): return "JSONEncodableError: Got incompatible type \(elementType) for key \(key)" case let .TransformerFailedError(key: key): @@ -77,6 +80,21 @@ public extension Array {//where Element: JSONEncodable { // Dictionary convenience methods +public extension Dictionary {//where Key: String, Value: JSONEncodable { + public func toJSON() throws -> AnyObject { + var result: [String: AnyObject] = [:] + for (k, item) in self { + if let item = item as? JSONEncodable { + result[String(k)] = try item.toJSON() + } + else { + throw JSONEncodableError.DictionaryIncompatibleTypeError(elementType: item.dynamicType) + } + } + return result + } +} + public extension Dictionary where Value: AnyObject { public mutating func encode(value: Any, key: Key) throws { let actualValue: Any @@ -107,6 +125,15 @@ public extension Dictionary where Value: AnyObject { self[key] = (result as! Value) } + // test for dictionary + else if let dict = actualValue as? JSONDictionary { + if dict.dictionaryIsJSONEncodable() { + let encodableDict = dict.dictionaryMadeJSONEncodable() + let result = try encodableDict.toJSON() + self[key] = (result as! Value) + } + } + // incompatible type else { throw JSONEncodableError.ChildIncompatibleTypeError(key: key as! String, elementType: actualValue.dynamicType) diff --git a/JSONCodable/JSONHelpers.swift b/JSONCodable/JSONHelpers.swift index bead3c0..3a3f5af 100644 --- a/JSONCodable/JSONHelpers.swift +++ b/JSONCodable/JSONHelpers.swift @@ -6,6 +6,27 @@ // Copyright © 2015 matthewcheok. All rights reserved. // +// Dictionary handling + +protocol JSONDictionary { + func dictionaryIsJSONEncodable() -> Bool + func dictionaryMadeJSONEncodable() -> [String: JSONEncodable] +} + +extension Dictionary : JSONDictionary { + func dictionaryIsJSONEncodable() -> Bool { + return Key.self is String.Type && Value.self is JSONEncodable.Type + } + + func dictionaryMadeJSONEncodable() -> [String: JSONEncodable] { + var dict: [String: JSONEncodable] = [:] + for (k, v) in self { + dict[String(k)] = v as? JSONEncodable + } + return dict + } +} + // Array handling protocol JSONArray { diff --git a/JSONCodable/JSONString.swift b/JSONCodable/JSONString.swift index f07f185..01543d7 100644 --- a/JSONCodable/JSONString.swift +++ b/JSONCodable/JSONString.swift @@ -10,12 +10,48 @@ import Foundation public extension JSONEncodable { public func toJSONString() throws -> String { - let json = try toJSON() - let data = try NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions(rawValue: 0)) - guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else { - return "" + switch self { + case let str as String: + return escapeJSONString(str) + case is Bool, is Int, is Float, is Double: + return String(self) + default: + let json = try toJSON() + let data = try NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions(rawValue: 0)) + guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else { + return "" + } + return string as String + } + } +} + +private func escapeJSONString(str: String) -> String { + var chars = String.CharacterView("\"") + for c in str.characters { + switch c { + case "\\": + chars.append("\\") + chars.append("\\") + case "\"": + chars.append("\\") + chars.append("\"") + default: + chars.append(c) + } + } + chars.append("\"") + return String(chars) +} + +public extension Optional where Wrapped: JSONEncodable { + public func toJSONString() throws -> String { + switch self { + case let .Some(jsonEncodable): + return try jsonEncodable.toJSONString() + case nil: + return "null" } - return string as String } }