Skip to content

Commit 9a4e8a5

Browse files
committed
JSON serializer and tests.
1 parent 14ed881 commit 9a4e8a5

File tree

2 files changed

+351
-1
lines changed

2 files changed

+351
-1
lines changed

Foundation/NSJSONSerialization.swift

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,26 @@ public class NSJSONSerialization : NSObject {
9797
/* 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.
9898
*/
9999
public class func dataWithJSONObject(obj: AnyObject, options opt: NSJSONWritingOptions) throws -> NSData {
100-
NSUnimplemented()
100+
guard obj is NSArray || obj is NSDictionary else {
101+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: [
102+
"NSDebugDescription" : "Top-level object was not NSArray or NSDictionary"
103+
])
104+
}
105+
106+
let result = NSMutableData()
107+
108+
var writer = JSONWriter(
109+
pretty: opt.contains(.PrettyPrinted),
110+
writer: { (str: String?) in
111+
if let str = str {
112+
result.appendBytes(str.bridge().cStringUsingEncoding(NSUTF8StringEncoding), length: str.lengthOfBytesUsingEncoding(NSUTF8StringEncoding))
113+
}
114+
}
115+
)
116+
117+
try writer.serializeJSON(obj)
118+
119+
return result
101120
}
102121

103122
/* 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 {
207226
}
208227
}
209228

229+
//MARK: - JSONSerializer
230+
private struct JSONWriter {
231+
232+
var indent = 0
233+
let pretty: Bool
234+
let writer: (String?) -> Void
235+
236+
init(pretty: Bool = false, writer: (String?) -> Void) {
237+
self.pretty = pretty
238+
self.writer = writer
239+
}
240+
241+
mutating func serializeJSON(obj: AnyObject) throws {
242+
if let str = obj as? NSString {
243+
try serializeString(str)
244+
}
245+
else if let num = obj as? NSNumber {
246+
try serializeNumber(num)
247+
}
248+
else if let array = obj as? NSArray {
249+
try serializeArray(array)
250+
}
251+
else if let dict = obj as? NSDictionary {
252+
try serializeDictionary(dict)
253+
}
254+
else if let null = obj as? NSNull {
255+
try serializeNull(null)
256+
}
257+
else {
258+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "Invalid object cannot be serialized"])
259+
}
260+
}
261+
262+
func serializeString(str: NSString) throws {
263+
let str = str.bridge()
264+
265+
writer("\"")
266+
for scalar in str.unicodeScalars {
267+
switch scalar {
268+
case "\"":
269+
writer("\\\"") // U+0022 quotation mark
270+
case "\\":
271+
writer("\\\\") // U+005C reverse solidus
272+
// U+002F solidus not escaped
273+
case "\u{8}":
274+
writer("\\b") // U+0008 backspace
275+
case "\u{c}":
276+
writer("\\f") // U+000C form feed
277+
case "\n":
278+
writer("\\n") // U+000A line feed
279+
case "\r":
280+
writer("\\r") // U+000D carriage return
281+
case "\t":
282+
writer("\\t") // U+0009 tab
283+
case "\u{0}"..."\u{f}":
284+
writer("\\u000\(String(scalar.value, radix: 16))") // U+0000 to U+000F
285+
case "\u{10}"..."\u{1f}":
286+
writer("\\u00\(String(scalar.value, radix: 16))") // U+0010 to U+001F
287+
default:
288+
writer(String(scalar))
289+
}
290+
}
291+
writer("\"")
292+
}
293+
294+
func serializeNumber(num: NSNumber) throws {
295+
if num.doubleValue.isInfinite || num.doubleValue.isNaN {
296+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "Number cannot be infinity or NaN"])
297+
}
298+
299+
// Cannot detect type information (e.g. bool) as there is no objCType property on NSNumber in Swift
300+
// So, just print the number
301+
302+
writer("\(num)")
303+
}
304+
305+
mutating func serializeArray(array: NSArray) throws {
306+
writer("[")
307+
if pretty {
308+
writer("\n")
309+
incAndWriteIndent()
310+
}
311+
312+
var first = true
313+
for elem in array.bridge() {
314+
if first {
315+
first = false
316+
} else if pretty {
317+
writer(",\n")
318+
writeIndent()
319+
} else {
320+
writer(",")
321+
}
322+
try serializeJSON(elem)
323+
}
324+
if pretty {
325+
writer("\n")
326+
decAndWriteIndent()
327+
}
328+
writer("]")
329+
}
330+
331+
mutating func serializeDictionary(dict: NSDictionary) throws {
332+
writer("{")
333+
if pretty {
334+
writer("\n")
335+
incAndWriteIndent()
336+
}
337+
338+
var first = true
339+
for (key, value) in dict.bridge() {
340+
if first {
341+
first = false
342+
} else if pretty {
343+
writer(",\n")
344+
writeIndent()
345+
} else {
346+
writer(",")
347+
}
348+
349+
if key is NSString {
350+
try serializeString(key as! NSString)
351+
} else {
352+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "NSDictionary key must be NSString"])
353+
}
354+
pretty ? writer(": ") : writer(":")
355+
try serializeJSON(value)
356+
}
357+
if pretty {
358+
writer("\n")
359+
decAndWriteIndent()
360+
}
361+
writer("}")
362+
}
363+
364+
func serializeNull(null: NSNull) throws {
365+
writer("null")
366+
}
367+
368+
let indentAmount = 2
369+
370+
mutating func incAndWriteIndent() {
371+
indent += indentAmount
372+
writeIndent()
373+
}
374+
375+
mutating func decAndWriteIndent() {
376+
indent -= indentAmount
377+
writeIndent()
378+
}
379+
380+
func writeIndent() {
381+
for _ in 0..<indent {
382+
writer(" ")
383+
}
384+
}
385+
}
386+
210387
//MARK: - JSONDeserializer
211388
private struct JSONReader {
212389

TestFoundation/TestNSJSONSerialization.swift

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class TestNSJSONSerialization : XCTestCase {
2929
return JSONObjectWithDataTests
3030
+ deserializationTests
3131
+ isValidJSONObjectTests
32+
+ serializationTests
3233
}
3334

3435
}
@@ -560,3 +561,175 @@ extension TestNSJSONSerialization {
560561
}
561562

562563
}
564+
565+
// MARK: - serializationTests
566+
extension TestNSJSONSerialization {
567+
568+
class var serializationTests: [(String, TestNSJSONSerialization -> () throws -> Void)] {
569+
return [
570+
("test_serialize_emptyObject", test_serialize_emptyObject),
571+
("test_serialize_null", test_serialize_null),
572+
("test_serialize_complexObject", test_serialize_complexObject),
573+
("test_nested_array", test_nested_array),
574+
("test_nested_dictionary", test_nested_dictionary),
575+
("test_serialize_number", test_serialize_number),
576+
("test_serialize_stringEscaping", test_serialize_stringEscaping),
577+
("test_serialize_invalid_json", test_serialize_invalid_json),
578+
]
579+
}
580+
581+
func trySerialize(obj: AnyObject) throws -> String {
582+
let data = try NSJSONSerialization.dataWithJSONObject(obj, options: [])
583+
guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else {
584+
XCTFail("Unable to create string")
585+
return ""
586+
}
587+
return string.bridge()
588+
}
589+
590+
func test_serialize_emptyObject() {
591+
let dict1 = [String: Any]().bridge()
592+
XCTAssertEqual(try trySerialize(dict1), "{}")
593+
594+
let dict2 = [String: NSNumber]().bridge()
595+
XCTAssertEqual(try trySerialize(dict2), "{}")
596+
597+
let dict3 = [String: String]().bridge()
598+
XCTAssertEqual(try trySerialize(dict3), "{}")
599+
600+
let array1 = [String]().bridge()
601+
XCTAssertEqual(try trySerialize(array1), "[]")
602+
603+
let array2 = [NSNumber]().bridge()
604+
XCTAssertEqual(try trySerialize(array2), "[]")
605+
}
606+
607+
func test_serialize_null() {
608+
let arr = [NSNull()].bridge()
609+
XCTAssertEqual(try trySerialize(arr), "[null]")
610+
611+
let dict = ["a":NSNull()].bridge()
612+
XCTAssertEqual(try trySerialize(dict), "{\"a\":null}")
613+
614+
let arr2 = [NSNull(), NSNull(), NSNull()].bridge()
615+
XCTAssertEqual(try trySerialize(arr2), "[null,null,null]")
616+
617+
let dict2 = [["a":NSNull()], ["b":NSNull()], ["c":NSNull()]].bridge()
618+
XCTAssertEqual(try trySerialize(dict2), "[{\"a\":null},{\"b\":null},{\"c\":null}]")
619+
}
620+
621+
func test_serialize_complexObject() {
622+
let jsonDict = ["a": 4].bridge()
623+
XCTAssertEqual(try trySerialize(jsonDict), "{\"a\":4}")
624+
625+
let jsonArr = [1, 2, 3, 4].bridge()
626+
XCTAssertEqual(try trySerialize(jsonArr), "[1,2,3,4]")
627+
628+
let jsonDict2 = ["a": [1,2]].bridge()
629+
XCTAssertEqual(try trySerialize(jsonDict2), "{\"a\":[1,2]}")
630+
631+
let jsonArr2 = ["a", "b", "c"].bridge()
632+
XCTAssertEqual(try trySerialize(jsonArr2), "[\"a\",\"b\",\"c\"]")
633+
634+
let jsonArr3 = [["a":1],["b":2]].bridge()
635+
XCTAssertEqual(try trySerialize(jsonArr3), "[{\"a\":1},{\"b\":2}]")
636+
637+
let jsonArr4 = [["a":NSNull()],["b":NSNull()]].bridge()
638+
XCTAssertEqual(try trySerialize(jsonArr4), "[{\"a\":null},{\"b\":null}]")
639+
}
640+
641+
func test_nested_array() {
642+
var arr = ["a"].bridge()
643+
XCTAssertEqual(try trySerialize(arr), "[\"a\"]")
644+
645+
arr = [["b"]].bridge()
646+
XCTAssertEqual(try trySerialize(arr), "[[\"b\"]]")
647+
648+
arr = [[["c"]]].bridge()
649+
XCTAssertEqual(try trySerialize(arr), "[[[\"c\"]]]")
650+
651+
arr = [[[["d"]]]].bridge()
652+
XCTAssertEqual(try trySerialize(arr), "[[[[\"d\"]]]]")
653+
}
654+
655+
func test_nested_dictionary() {
656+
var dict = ["a":1].bridge()
657+
XCTAssertEqual(try trySerialize(dict), "{\"a\":1}")
658+
659+
dict = ["a":["b":1]].bridge()
660+
XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":1}}")
661+
662+
dict = ["a":["b":["c":1]]].bridge()
663+
XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":{\"c\":1}}}")
664+
665+
dict = ["a":["b":["c":["d":1]]]].bridge()
666+
XCTAssertEqual(try trySerialize(dict), "{\"a\":{\"b\":{\"c\":{\"d\":1}}}}")
667+
}
668+
669+
func test_serialize_number() {
670+
var json = [1, 1.1, 0, -2].bridge()
671+
XCTAssertEqual(try trySerialize(json), "[1,1.1,0,-2]")
672+
673+
// Cannot generate "true"/"false" currently
674+
json = [NSNumber(bool:false),NSNumber(bool:true)].bridge()
675+
XCTAssertEqual(try trySerialize(json), "[0,1]")
676+
}
677+
678+
func test_serialize_stringEscaping() {
679+
var json = ["foo"].bridge()
680+
XCTAssertEqual(try trySerialize(json), "[\"foo\"]")
681+
682+
json = ["a\0"].bridge()
683+
XCTAssertEqual(try trySerialize(json), "[\"a\\u0000\"]")
684+
685+
json = ["b\\"].bridge()
686+
XCTAssertEqual(try trySerialize(json), "[\"b\\\\\"]")
687+
688+
json = ["c\t"].bridge()
689+
XCTAssertEqual(try trySerialize(json), "[\"c\\t\"]")
690+
691+
json = ["d\n"].bridge()
692+
XCTAssertEqual(try trySerialize(json), "[\"d\\n\"]")
693+
694+
json = ["e\r"].bridge()
695+
XCTAssertEqual(try trySerialize(json), "[\"e\\r\"]")
696+
697+
json = ["f\""].bridge()
698+
XCTAssertEqual(try trySerialize(json), "[\"f\\\"\"]")
699+
700+
json = ["g\'"].bridge()
701+
XCTAssertEqual(try trySerialize(json), "[\"g\'\"]")
702+
703+
json = ["h\u{7}"].bridge()
704+
XCTAssertEqual(try trySerialize(json), "[\"h\\u0007\"]")
705+
706+
json = ["i\u{1f}"].bridge()
707+
XCTAssertEqual(try trySerialize(json), "[\"i\\u001f\"]")
708+
}
709+
710+
func test_serialize_invalid_json() {
711+
let str = "Invalid JSON".bridge()
712+
do {
713+
let _ = try trySerialize(str)
714+
XCTFail("Top-level JSON object cannot be string")
715+
} catch {
716+
// should get here
717+
}
718+
719+
let double = NSNumber(double: 1.2)
720+
do {
721+
let _ = try trySerialize(double)
722+
XCTFail("Top-level JSON object cannot be double")
723+
} catch {
724+
// should get here
725+
}
726+
727+
let dict = [NSNumber(double: 1.2):"a"].bridge()
728+
do {
729+
let _ = try trySerialize(dict)
730+
XCTFail("Dictionary keys must be strings")
731+
} catch {
732+
// should get here
733+
}
734+
}
735+
}

0 commit comments

Comments
 (0)