diff --git a/Foundation/NSJSONSerialization.swift b/Foundation/NSJSONSerialization.swift index 0e56b68702..5eac7e81ea 100644 --- a/Foundation/NSJSONSerialization.swift +++ b/Foundation/NSJSONSerialization.swift @@ -97,7 +97,26 @@ public class NSJSONSerialization : NSObject { /* Generate JSON data from a Foundation object. If the object will not produce valid JSON then an exception will be thrown. Setting the NSJSONWritingPrettyPrinted option will generate JSON with whitespace designed to make the output more readable. If that option is not set, the most compact possible JSON will be generated. If an error occurs, the error parameter will be set and the return value will be nil. The resulting data is a encoded in UTF-8. */ public class func dataWithJSONObject(obj: AnyObject, options opt: NSJSONWritingOptions) throws -> NSData { - NSUnimplemented() + guard obj is NSArray || obj is NSDictionary else { + throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: [ + "NSDebugDescription" : "Top-level object was not NSArray or NSDictionary" + ]) + } + + let result = NSMutableData() + + var writer = JSONWriter( + pretty: opt.contains(.PrettyPrinted), + writer: { (str: String?) in + if let str = str { + result.appendBytes(str.bridge().cStringUsingEncoding(NSUTF8StringEncoding), length: str.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)) + } + } + ) + + try writer.serializeJSON(obj) + + return result } /* Create a Foundation object from JSON data. Set the NSJSONReadingAllowFragments option if the parser should allow top-level objects that are not an NSArray or NSDictionary. Setting the NSJSONReadingMutableContainers option will make the parser generate mutable NSArrays and NSDictionaries. Setting the NSJSONReadingMutableLeaves option will make the parser generate mutable NSString objects. If an error occurs during the parse, then the error parameter will be set and the result will be nil. @@ -207,6 +226,164 @@ internal extension NSJSONSerialization { } } +//MARK: - JSONSerializer +private struct JSONWriter { + + var indent = 0 + let pretty: Bool + let writer: (String?) -> Void + + init(pretty: Bool = false, writer: (String?) -> Void) { + self.pretty = pretty + self.writer = writer + } + + mutating func serializeJSON(obj: AnyObject) throws { + if let str = obj as? NSString { + try serializeString(str) + } + else if let num = obj as? NSNumber { + try serializeNumber(num) + } + else if let array = obj as? NSArray { + try serializeArray(array) + } + else if let dict = obj as? NSDictionary { + try serializeDictionary(dict) + } + else if let null = obj as? NSNull { + try serializeNull(null) + } + else { + throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "Invalid object cannot be serialized"]) + } + } + + func serializeString(str: NSString) throws { + let str = str.bridge() + + writer("\"") + for scalar in str.unicodeScalars { + switch scalar { + case "\"": + writer("\\\"") // U+0022 quotation mark + case "\\": + writer("\\\\") // U+005C reverse solidus + // U+002F solidus not escaped + case "\u{8}": + writer("\\b") // U+0008 backspace + case "\u{c}": + writer("\\f") // U+000C form feed + case "\n": + writer("\\n") // U+000A line feed + case "\r": + writer("\\r") // U+000D carriage return + case "\t": + writer("\\t") // U+0009 tab + case "\u{0}"..."\u{f}": + writer("\\u000\(String(scalar.value, radix: 16))") // U+0000 to U+000F + case "\u{10}"..."\u{1f}": + writer("\\u00\(String(scalar.value, radix: 16))") // U+0010 to U+001F + default: + writer(String(scalar)) + } + } + writer("\"") + } + + func serializeNumber(num: NSNumber) throws { + if num.doubleValue.isInfinite || num.doubleValue.isNaN { + throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "Number cannot be infinity or NaN"]) + } + + // Cannot detect type information (e.g. bool) as there is no objCType property on NSNumber in Swift + // So, just print the number + + writer("\(num)") + } + + mutating func serializeArray(array: NSArray) throws { + writer("[") + if pretty { + writer("\n") + incAndWriteIndent() + } + + var first = true + for elem in array.bridge() { + if first { + first = false + } else if pretty { + writer(",\n") + writeIndent() + } else { + writer(",") + } + try serializeJSON(elem) + } + if pretty { + writer("\n") + decAndWriteIndent() + } + writer("]") + } + + mutating func serializeDictionary(dict: NSDictionary) throws { + writer("{") + if pretty { + writer("\n") + incAndWriteIndent() + } + + var first = true + for (key, value) in dict.bridge() { + if first { + first = false + } else if pretty { + writer(",\n") + writeIndent() + } else { + writer(",") + } + + if key is NSString { + try serializeString(key as! NSString) + } else { + throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "NSDictionary key must be NSString"]) + } + pretty ? writer(": ") : writer(":") + try serializeJSON(value) + } + if pretty { + writer("\n") + decAndWriteIndent() + } + writer("}") + } + + func serializeNull(null: NSNull) throws { + writer("null") + } + + let indentAmount = 2 + + mutating func incAndWriteIndent() { + indent += indentAmount + writeIndent() + } + + mutating func decAndWriteIndent() { + indent -= indentAmount + writeIndent() + } + + func writeIndent() { + for _ in 0.. () throws -> Void)] { + return [ + ("test_serialize_emptyObject", test_serialize_emptyObject), + ("test_serialize_null", test_serialize_null), + ("test_serialize_complexObject", test_serialize_complexObject), + ("test_nested_array", test_nested_array), + ("test_nested_dictionary", test_nested_dictionary), + ("test_serialize_number", test_serialize_number), + ("test_serialize_stringEscaping", test_serialize_stringEscaping), + ("test_serialize_invalid_json", test_serialize_invalid_json), + ] + } + + func trySerialize(obj: AnyObject) throws -> String { + let data = try NSJSONSerialization.dataWithJSONObject(obj, options: []) + guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else { + XCTFail("Unable to create string") + return "" + } + return string.bridge() + } + + func test_serialize_emptyObject() { + let dict1 = [String: Any]().bridge() + XCTAssertEqual(try trySerialize(dict1), "{}") + + let dict2 = [String: NSNumber]().bridge() + XCTAssertEqual(try trySerialize(dict2), "{}") + + let dict3 = [String: String]().bridge() + XCTAssertEqual(try trySerialize(dict3), "{}") + + let array1 = [String]().bridge() + XCTAssertEqual(try trySerialize(array1), "[]") + + let array2 = [NSNumber]().bridge() + XCTAssertEqual(try trySerialize(array2), "[]") + } + + func test_serialize_null() { + let arr = [NSNull()].bridge() + XCTAssertEqual(try trySerialize(arr), "[null]") + + let dict = ["a":NSNull()].bridge() + XCTAssertEqual(try trySerialize(dict), "{\"a\":null}") + + let arr2 = [NSNull(), NSNull(), NSNull()].bridge() + XCTAssertEqual(try trySerialize(arr2), "[null,null,null]") + + let dict2 = [["a":NSNull()], ["b":NSNull()], ["c":NSNull()]].bridge() + XCTAssertEqual(try trySerialize(dict2), "[{\"a\":null},{\"b\":null},{\"c\":null}]") + } + + func test_serialize_complexObject() { + let jsonDict = ["a": 4].bridge() + XCTAssertEqual(try trySerialize(jsonDict), "{\"a\":4}") + + let jsonArr = [1, 2, 3, 4].bridge() + XCTAssertEqual(try trySerialize(jsonArr), "[1,2,3,4]") + + let jsonDict2 = ["a": [1,2]].bridge() + XCTAssertEqual(try trySerialize(jsonDict2), "{\"a\":[1,2]}") + + let jsonArr2 = ["a", "b", "c"].bridge() + XCTAssertEqual(try trySerialize(jsonArr2), "[\"a\",\"b\",\"c\"]") + + let jsonArr3 = [["a":1],["b":2]].bridge() + XCTAssertEqual(try trySerialize(jsonArr3), "[{\"a\":1},{\"b\":2}]") + + let jsonArr4 = [["a":NSNull()],["b":NSNull()]].bridge() + XCTAssertEqual(try trySerialize(jsonArr4), "[{\"a\":null},{\"b\":null}]") + } + + func test_nested_array() { + var arr = ["a"].bridge() + XCTAssertEqual(try trySerialize(arr), "[\"a\"]") + + arr = [["b"]].bridge() + XCTAssertEqual(try trySerialize(arr), "[[\"b\"]]") + + arr = [[["c"]]].bridge() + XCTAssertEqual(try trySerialize(arr), "[[[\"c\"]]]") + + arr = [[[["d"]]]].bridge() + XCTAssertEqual(try trySerialize(arr), "[[[[\"d\"]]]]") + } + + func test_nested_dictionary() { + var dict = ["a":1].bridge() + XCTAssertEqual(try trySerialize(dict), "{\"a\":1}") + + dict = ["a":["b":1]].bridge() + XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":1}}") + + dict = ["a":["b":["c":1]]].bridge() + XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":{\"c\":1}}}") + + dict = ["a":["b":["c":["d":1]]]].bridge() + XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":{\"c\":{\"d\":1}}}}") + } + + func test_serialize_number() { + var json = [1, 1.1, 0, -2].bridge() + XCTAssertEqual(try trySerialize(json), "[1,1.1,0,-2]") + + // Cannot generate "true"/"false" currently + json = [NSNumber(bool:false),NSNumber(bool:true)].bridge() + XCTAssertEqual(try trySerialize(json), "[0,1]") + } + + func test_serialize_stringEscaping() { + var json = ["foo"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"foo\"]") + + json = ["a\0"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"a\\u0000\"]") + + json = ["b\\"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"b\\\\\"]") + + json = ["c\t"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"c\\t\"]") + + json = ["d\n"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"d\\n\"]") + + json = ["e\r"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"e\\r\"]") + + json = ["f\""].bridge() + XCTAssertEqual(try trySerialize(json), "[\"f\\\"\"]") + + json = ["g\'"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"g\'\"]") + + json = ["h\u{7}"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"h\\u0007\"]") + + json = ["i\u{1f}"].bridge() + XCTAssertEqual(try trySerialize(json), "[\"i\\u001f\"]") + } + + func test_serialize_invalid_json() { + let str = "Invalid JSON".bridge() + do { + let _ = try trySerialize(str) + XCTFail("Top-level JSON object cannot be string") + } catch { + // should get here + } + + let double = NSNumber(double: 1.2) + do { + let _ = try trySerialize(double) + XCTFail("Top-level JSON object cannot be double") + } catch { + // should get here + } + + let dict = [NSNumber(double: 1.2):"a"].bridge() + do { + let _ = try trySerialize(dict) + XCTFail("Dictionary keys must be strings") + } catch { + // should get here + } + } +}