Functional JSON Parsing in Swift
//MARK: examples | |
extension JSONValue { | |
static func url(value: JSONType) -> JSONResult<NSURL> { | |
return cast(value).map({NSURL(string: $0)}) | |
} | |
} | |
extension JSONValue { | |
static func gender(value: JSONType) -> JSONResult<Gender> { | |
return cast(value).map({ (string: String) -> Gender in | |
var gender : Gender = .Unknown | |
if(string == "male") { | |
gender = .Male | |
} else if (string == "female") { | |
gender = .Female | |
} | |
return gender | |
}) | |
} | |
} | |
struct Address: JSONModel { | |
var city: String | |
var street: String | |
var number: NSNumber | |
static func create(city: String)(street: String)(number: Int) -> Address { | |
return Address(city: city,street: street, number: number) | |
} | |
static func decode(json: JSONObject) -> JSONResult<Address> { | |
return Address.create <^> json.value("city") | |
<*> json.value("street") | |
<*> json.value("number") | |
} | |
} | |
enum Gender { | |
case Male | |
case Female | |
case Unknown | |
} | |
struct User: JSONModel { | |
var firstName: String | |
var lastName: String | |
var gender: Gender | |
var websites: [NSURL] | |
var address: Address | |
var awesome: Bool? | |
static func create(firstName: String) | |
(lastName: String) | |
(gender: Gender) | |
(websites: [NSURL]) | |
(address: Address) | |
(awesome: Bool?) | |
-> User { | |
return User(firstName: firstName,lastName:lastName, gender: gender, websites: websites, address: address, awesome: awesome) | |
} | |
static func decode(json: JSONObject) -> JSONResult<User> { | |
return User.create <^> json["firstName"] >>= JSONValue.string | |
<*> json.value("lastName") | |
<*> json["gender"] >>= JSONValue.gender | |
<*> json["websites"] >>= JSONValue.url | |
<*> json["address"] >>= parse | |
<*> json.optional("awesome") >>= JSONValue.bool | |
} | |
} | |
let test = ["{\"firstName\":\"Niels\",\"lastName\":\"van Hoorn\",\"gender\":\"male\",\"websites\":[\"http://zekerwaar.nl\",\"http://codecoverage.nl\",\"http://nvh.io\"],\"awesome\":true,\"address\":{\"street\":\"Neude\",\"number\":4,\"city\":\"Utrecht\"}}" | |
,"{\"firstName\":\"Chris\",\"lastName\":\"Eidhof\",\"gender\":\"male\",\"websites\":[\"http://objc.io\",\"http://chris.eidhof.nl\"]}"] | |
for json in test { | |
if let jsonObject: JSONType = parseString(json) { | |
println(jsonObject) | |
let result : JSONResult<User> = parse(jsonObject) | |
switch(result) { | |
case let .Failure(error): | |
println(error) | |
default:() | |
} | |
var error : NSError? = nil | |
if let user : User? = parse(jsonObject, &error) { | |
user | |
error | |
println(user) | |
println(user?.awesome) | |
println(user?.address.city) | |
} | |
} | |
} | |
let list = "[{\"firstName\":\"Niels\",\"lastName\":\"van Hoorn\",\"gender\":\"male\",\"websites\":[\"http://zekerwaar.nl\",\"http://codecoverage.nl\",\"http://nvh.io\"],\"awesome\":true,\"address\":{\"street\":\"Neude\",\"number\":4,\"city\":\"Utrecht\"}},{\"firstName\":\"Chris\",\"lastName\":\"Eidhof\",\"gender\":\"male\",\"websites\":[\"http://objc.io\",\"http://chris.eidhof.nl\"],\"address\":{\"street\":\"a\",\"city\":\"Berlin\",\"number\":2}}]" | |
if let jsonObject: JSONType = parseString(list) { | |
println(jsonObject) | |
if let arr = jsonObject as? [JSONType] { | |
let result : JSONResult<[User]> = parseList(arr) | |
switch(result) { | |
case let .Failure(error): | |
println(error) | |
case let .Success(box): | |
println(box.value) | |
} | |
} | |
} |
import Foundation | |
public typealias JSONType = AnyObject | |
func fail<A>(message: String) -> JSONResult<A> { | |
return .Failure(NSError(domain: "ParseJSONDomain", code: 0, userInfo: [NSLocalizedDescriptionKey:message])) | |
} | |
func pure<T>(value: T) -> JSONResult<T> { | |
return .Success(Box(value)) | |
} | |
func cast<T,U>(value: T) -> JSONResult<U> { | |
if let a = value as? U { | |
return pure(a) | |
} else { | |
return fail("Failed to cast \(value) to \(U.self)") | |
} | |
} | |
func sequence<T>(list: [JSONResult<T>]) -> JSONResult<[T]> { | |
var result : [T] = [] | |
for element in list { | |
switch(element) { | |
case let .Success(box): | |
result.append(box.value) | |
case let .Failure(error): | |
return fail("Sequence failed: \(error)") | |
} | |
} | |
return pure(result) | |
} | |
func mapM<T,U>(transform: T -> JSONResult<U>, list: [T]) -> JSONResult<[U]> { | |
return sequence(list.map(transform)) | |
} | |
func forM<T,U>(list: [T], transform: T -> JSONResult<U>) -> JSONResult<[U]> { | |
return mapM(transform, list) | |
} | |
func optional<T>(value: T) -> T? { | |
let v : T? = value | |
return v | |
} | |
//Boxing of variable is needed because Generic Typed Enum's aren't (yet?) supported in Swift | |
class Box<T> { | |
let value: T! | |
init(_ value: T) { self.value = value } | |
} | |
//Functor extension | |
extension Box { | |
func map<U>(transform: (T) -> U) -> Box<U> { | |
return Box<U>(transform(self.value)) | |
} | |
} | |
enum JSONResult<T> { | |
case Success(Box<T>) | |
case Failure(NSError) | |
} | |
//Functor extension | |
extension JSONResult { | |
func map<U> (transform: T -> U) -> JSONResult<U> { | |
switch self { | |
case let JSONResult.Success(box): | |
return JSONResult<U>.Success(box.map(transform)) | |
case let JSONResult.Failure(error): | |
return JSONResult<U>.Failure(error) | |
} | |
} | |
} | |
//Applicative extension | |
extension JSONResult { | |
func sequence<U>(transform: JSONResult<T -> U>) -> JSONResult<U> { | |
switch (transform) { | |
case let .Success(transformBox): | |
switch(self) { | |
case let .Success(box): | |
return .Success(box.map(transformBox.value)) | |
case let .Failure(error): | |
return .Failure(error) | |
} | |
case let .Failure(error): | |
return .Failure(error) | |
} | |
} | |
} | |
//Monadic Extension | |
extension JSONResult { | |
func bind<U>(f : T -> JSONResult<U>) -> JSONResult<U> { | |
switch(self) { | |
case let .Success(box): | |
return f(box.value) | |
case let .Failure(error): | |
return .Failure(error) | |
} | |
} | |
} | |
//MARK: operators | |
infix operator >>= { associativity left precedence 200 } | |
infix operator <^> { associativity left precedence 150 } | |
infix operator <*> { associativity left precedence 150 } | |
func >>= <T,U> (value : JSONResult<T>, transform: T -> JSONResult<U>) -> JSONResult<U> { | |
return value.bind(transform) | |
} | |
//Array bind | |
func >>= <U> (value : JSONResult<JSONType>, transform: JSONType -> JSONResult<U>) -> JSONResult<[U]> { | |
return value.bind(cast).bind({forM($0, transform)}) | |
} | |
//Optional bind | |
func >>= <U> (value : JSONResult<JSONType>, transform: JSONType -> JSONResult<U>) -> JSONResult<U?> { | |
return value.bind( {(v: JSONType?) -> JSONResult<U?> in | |
if let a: JSONType = v { | |
if let b = a as? NSNull { | |
return pure(nil) | |
} | |
return transform(a).map(optional) | |
} else { | |
return pure(nil) | |
} | |
}) | |
} | |
func <*><T, U>(transform: JSONResult<T -> U>, value: JSONResult<T>) -> JSONResult<U> { | |
return value.sequence(transform) | |
} | |
//<$> in Haskell | |
func <^><T, U>(transform: T -> U, value: JSONResult<T>) -> JSONResult<U> { | |
return value.map(transform) | |
} | |
protocol JSONDecodable { | |
class func decode(jsonObject: JSONObject) -> JSONResult<Self> | |
} | |
protocol JSONModel : JSONDecodable { | |
} | |
//MARK: value parsing | |
struct JSONObject { | |
var object : [String:JSONType] | |
subscript(key: String) -> JSONResult<JSONType> { | |
if let value: JSONType = object[key] { | |
return pure(value) | |
} else { | |
return fail("JSON Object \(object) has no key '\(key)'") | |
} | |
} | |
func optional(key:String) -> JSONResult<JSONType> { | |
if let value: JSONType = object[key] { | |
return pure(value) | |
} else { | |
return pure(NSNull()) | |
} | |
} | |
func value<T>(key: String) -> JSONResult<T> { | |
return self[key].bind(JSONValue.decode) | |
} | |
} | |
struct JSONValue { | |
static func decode<T>(value: JSONType) -> JSONResult<T> { | |
return cast(value) | |
} | |
static func string(value: JSONType) -> JSONResult<String> { | |
return decode(value) | |
} | |
static func number(value: JSONType) -> JSONResult<NSNumber> { | |
return decode(value) | |
} | |
static func number(value: JSONType) -> JSONResult<Int> { | |
return self.number(value) >>= {pure($0.integerValue)} | |
} | |
static func number(value: JSONType) -> JSONResult<Double> { | |
return self.number(value) >>= {pure($0.doubleValue)} | |
} | |
static func number(value: JSONType) -> JSONResult<Float> { | |
return self.number(value) >>= {pure($0.floatValue)} | |
} | |
static func bool(value: JSONType) -> JSONResult<Bool> { | |
return self.number(value) >>= {pure($0.boolValue)} | |
} | |
static func object(value: JSONType) -> JSONResult<[String:JSONType]> { | |
return decode(value) | |
} | |
} | |
//MARK: parsing | |
func parseString(jsonString: String) -> JSONType! { | |
var json : JSONType? = nil | |
if let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) { | |
json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) | |
} | |
return json | |
} | |
func parse<A: JSONDecodable>(json: JSONType) -> JSONResult<A> { | |
return cast(json).map({JSONObject(object: $0)}).bind({ A.decode($0) }) | |
} | |
func parseList<A: JSONDecodable>(list: [JSONType]) -> JSONResult<[A]> { | |
return forM(list, parse) | |
} | |
func parse<A: JSONDecodable>(json: JSONType, inout error: NSError?) -> A? { | |
let result : JSONResult<A> = parse(json) | |
var returnValue: A? = nil | |
switch(result) { | |
case let .Success(box): | |
returnValue = box.value | |
case let .Failure(e): | |
error = e | |
} | |
return returnValue | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
This is heavily inspired by @chriseidhof's Parsing JSON in Swift and @tonyd256's Efficient JSON in Swift with Functional Concepts and Generics