Skip to content

Instantly share code, notes, and snippets.

@stefanlindbohm
Last active December 30, 2015 12:06
Show Gist options
  • Save stefanlindbohm/7e2a7da470cbd98920bd to your computer and use it in GitHub Desktop.
Save stefanlindbohm/7e2a7da470cbd98920bd to your computer and use it in GitHub Desktop.
Extensible JSON interface in Swift enforced by types
//
// JSON.swift
// BonAppetit
//
// Created by Stefan Lindbohm on 17/02/15.
// Copyright (c) 2015 Stefan Lindbohm. All rights reserved.
//
import Foundation
protocol JSONValue {
func JSONDictionaryValue() -> AnyObject
}
protocol BuildableJSONValue: JSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Self?
}
extension String: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> String? {
return JSONDictionaryValue as? String
}
func JSONDictionaryValue() -> AnyObject { return self }
}
extension Int: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Int? {
return JSONDictionaryValue as? Int
}
func JSONDictionaryValue() -> AnyObject { return self }
}
extension Float: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Float? {
return JSONDictionaryValue as? Float
}
func JSONDictionaryValue() -> AnyObject { return self }
}
extension Double: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Double? {
return JSONDictionaryValue as? Double
}
func JSONDictionaryValue() -> AnyObject { return self }
}
extension Bool: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Bool? {
return JSONDictionaryValue as? Bool
}
func JSONDictionaryValue() -> AnyObject { return self }
}
extension NSUUID: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Self? {
guard let string = JSONDictionaryValue as? String else {
return nil
}
return self.init(UUIDString: string)
}
func JSONDictionaryValue() -> AnyObject {
return self.UUIDString
}
}
extension NSDate: BuildableJSONValue {
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> Self? {
guard let string = JSONDictionaryValue as? String else { return nil }
guard let date = NSDateFormatter.dateFromISO8601String(string) else {
fatalError("Value is not parsable as date")
}
return self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
}
func JSONDictionaryValue() -> AnyObject {
return NSDateFormatter.ISO8601String(date: self)
}
}
// TODO: Constrain this to [JSONValue] when supported in Swift. Might also remove the need for
// separation between JSONValue and BuildableJSONValue
extension Array: JSONValue {
func JSONDictionaryValue() -> AnyObject {
return self.map { element in
guard let element = element as? BuildableJSONValue else {
fatalError("Array used as JSONMember contains element not conforming to JSONMember")
}
return element.JSONDictionaryValue()
} as [AnyObject]
}
}
struct JSONObject: BuildableJSONValue {
private let JSONDictionary: [ String : AnyObject ]
// MARK: Lifecycle
init?(JSONData: NSData) {
do {
guard let JSONDictionary = try NSJSONSerialization.JSONObjectWithData(JSONData, options: []) as? Dictionary<String, AnyObject> else {
fatalError("Unknown type encountered when deserializing JSON")
}
self.JSONDictionary = JSONDictionary
} catch let error as NSError where error.code == NSPropertyListReadCorruptError {
return nil
} catch let error as NSError {
fatalError("Unknown error occurred while deserializing JSON: \(error.description)")
}
}
init(members: [String : JSONValue?]) {
self.JSONDictionary = JSONObject.buildJSONDictionaryFromMembers(members)
}
private init(JSONDictionary: Dictionary<String, AnyObject>) {
self.JSONDictionary = JSONDictionary
}
// MARK: Nested objects
subscript(name: String) -> JSONObject? {
return get(name)
}
// MARK: Members
func get<T: BuildableJSONValue>(name: String) -> T? {
return T.buildFromJSONDictionaryValue(concreteValue(name))
}
func get<T: BuildableJSONValue>(name: String) -> [T]? {
guard let values = concreteValue(name) as? [AnyObject] else {
return nil
}
return values.map { element in
guard let value = T.buildFromJSONDictionaryValue(element) else {
fatalError("Array contains elements not compatible with type \(T.self)")
}
return value
}
}
// MARK: Serialization
func JSONString() -> NSData {
do {
return try NSJSONSerialization.dataWithJSONObject(JSONDictionary, options: [])
} catch let error as NSError {
fatalError("Serialization failed with error: \(error.description)")
}
}
// MARK: BuildableJSONValue
static func buildFromJSONDictionaryValue(JSONDictionaryValue: AnyObject) -> JSONObject? {
guard let members = JSONDictionaryValue as? Dictionary<String, AnyObject> else {
return nil
}
return JSONObject(JSONDictionary: members)
}
func JSONDictionaryValue() -> AnyObject {
return JSONDictionary
}
// MARK: Private helpers
private func concreteValue(name: String) -> AnyObject {
guard let value = JSONDictionary[name] else {
fatalError("Object contains no member with name \(name)")
}
return value
}
private static func buildJSONDictionaryFromMembers(members: [ String : JSONValue? ]) -> [ String : AnyObject ] {
var newDictionary = [ String: AnyObject ]()
for (name, value) in members {
if let value = value {
newDictionary[name] = value.JSONDictionaryValue()
} else {
newDictionary[name] = NSNull()
}
}
return newDictionary
}
}
//
// JSONSpec.swift
// BonAppetit
//
// Created by Stefan Lindbohm on 27/12/15.
// Copyright © 2015 Stefan Lindbohm. All rights reserved.
//
import Quick
import Nimble
@testable import BonAppetit
class JSONObjectSpec: QuickSpec {
override func spec() {
describe("init?(data: NSData)") {
it("initializes with empty JSON object as string") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{}"))
expect(json).to(beTruthy())
}
it("doesn't initialize for empty strings") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding(""))
expect(json).to(beNil())
}
it("initializes with string values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"string\": \"foo\" }"))
let actual: String? = json?.get("string")
expect(actual).to(equal("foo"))
}
it("initializes with integer values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"integer\": 1 }"))
let actual: Int? = json?.get("integer")
expect(actual).to(equal(1))
}
it("initializes with double values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"double\": 3.14 }"))
let actual: Double? = json?.get("double")
expect(actual).to(equal(3.14))
}
it("initializes with boolean values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"yes\": true, \"no\": false }"))
let actualYes: Bool? = json?.get("yes")
expect(actualYes).to(beTrue())
let actualNo: Bool? = json?.get("no")
expect(actualNo).to(beFalse())
}
it("initializes with null values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"nothing\": null }"))
expect(json).to(beTruthy())
let actual: Int? = json?.get("nothing")
expect(actual).to(beNil())
}
it("initializes with arrays") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"array\": [ 1, 2 ] }"))
let actual: [Int]? = json?.get("array")
expect(actual).to(equal([ 1, 2 ]))
}
it("initializes with nested objects") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"object\": { \"foo\": \"bar\" } }"))
let actual: String? = json?["object"]?.get("foo")
expect(actual).to(equal("bar"))
}
it("initializes with object arrays") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"objects\": [ { \"foo\": \"bar\" }, { \"foo\": \"baz\" } ] }"))
let array: [JSONObject]? = json?.get("objects")
expect(array?.count).to(equal(2))
expect(array?[0].get("foo")).to(equal("bar"))
expect(array?[1].get("foo")).to(equal("baz"))
}
it("initializes with UUID values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"uuid\": \"27B1FA23-0255-42D5-BC3E-98BEED67FC8D\" }"))
let actual: NSUUID? = json?.get("uuid")
expect(actual).to(equal(NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D")))
}
it("initializes with date values") {
let json = JSONObject(JSONData: self.dataUsingUnicodeEncoding("{ \"date\": \"2016-01-01T00:00:00Z\" }"))
let actual: NSDate? = json?.get("date")
expect(actual).to(equal(self.UTCDate(2016, month: 1, day: 1)))
}
}
describe("init(members: Dictionary<String, Any?>") {
it("initializes with string values") {
let json = JSONObject(members: [ "string": "foo" ])
let actual: String? = json.get("string")
expect(actual).to(equal("foo"))
}
it("initializes with integer values") {
let json = JSONObject(members: [ "integer": 1 ])
let actual: Int? = json.get("integer")
expect(actual).to(equal(1))
}
it("initializes with double values") {
let json = JSONObject(members: [ "double": 3.14 ])
let actual: Double? = json.get("double")
expect(actual).to(equal(3.14))
}
it("initializes with boolean values") {
let json = JSONObject(members: [ "yes": true, "no": false ])
let actualYes: Bool? = json.get("yes")
expect(actualYes).to(beTrue())
let actualNo: Bool? = json.get("no")
expect(actualNo).to(beFalse())
}
it("initializes with null values") {
let json = JSONObject(members: [ "nothing": nil ])
expect(json).to(beTruthy())
let actual: Int? = json.get("nothing")
expect(actual).to(beNil())
}
it("initializes with UUID values") {
let json = JSONObject(members: [ "uuid": NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D") ])
let actual: NSUUID? = json.get("uuid")
expect(actual).to(equal(NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D")))
}
it("initializes with NSDate values") {
let json = JSONObject(members: [ "date": self.UTCDate(2016, month: 1, day: 1) ])
let actual: NSDate? = json.get("date")
expect(actual).to(equal(self.UTCDate(2016, month: 1, day: 1)))
}
it("initializes with arrays") {
let json = JSONObject(members: [ "array": [ 1, 2 ] ])
let actual: [Int]? = json.get("array")
expect(actual).to(equal([ 1, 2 ]))
}
it("initializes with nested JSON instances") {
let json = JSONObject(members: [ "object": JSONObject(members: [ "uuid": NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D") ]) ])
let actual: NSUUID? = json["object"]?.get("uuid")
expect(actual).to(equal(NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D")))
}
}
describe("JSONString()") {
it("serializes empty object") {
let json = JSONObject(members: [ String: JSONValue? ]())
expect(self.stringUsingUnicodeEncoding(json.JSONString())).to(equal("{}"))
}
it("serializes UUID") {
let json = JSONObject(members: [ "uuid": NSUUID(UUIDString: "27B1FA23-0255-42D5-BC3E-98BEED67FC8D") ])
expect(self.stringUsingUnicodeEncoding(json.JSONString())).to(equal("{\"uuid\":\"27B1FA23-0255-42D5-BC3E-98BEED67FC8D\"}"))
}
it("serializes date") {
let json = JSONObject(members: [ "date": self.UTCDate(2016, month: 1, day: 1) ])
expect(self.stringUsingUnicodeEncoding(json.JSONString())).to(equal("{\"date\":\"2016-01-01T00:00:00Z\"}"))
}
it("serializes nil") {
let json = JSONObject(members: [ "nothing": nil ])
expect(self.stringUsingUnicodeEncoding(json.JSONString())).to(equal("{\"nothing\":null}"))
}
it("serializes complex structures with strings and integers") {
let json = JSONObject(members: [
"array": [ 1, 2 ],
"object": JSONObject(members: [ "foo": "bar" ])
])
expect(self.stringUsingUnicodeEncoding(json.JSONString())).to(equal("{\"array\":[1,2],\"object\":{\"foo\":\"bar\"}}"))
}
}
}
private func dataUsingUnicodeEncoding(string: String) -> NSData {
return string.dataUsingEncoding(NSUTF8StringEncoding)!
}
private func stringUsingUnicodeEncoding(data: NSData) -> String {
guard let string = NSString(data: data, encoding: NSUTF8StringEncoding) else {
fatalError("Could not read string from data")
}
return string as String
}
private func UTCDate(year: Int, month: Int, day: Int) -> NSDate {
let calendar = NSCalendar.currentCalendar()
calendar.timeZone = NSTimeZone(abbreviation: "UTC")!
return calendar.dateWithEra(1, year: year, month: month, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0)!
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment