Last active
August 29, 2015 14:07
-
-
Save nvh/ff72db507d760a964ab2 to your computer and use it in GitHub Desktop.
Functional JSON Parsing in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 is heavily inspired by @chriseidhof's Parsing JSON in Swift and @tonyd256's Efficient JSON in Swift with Functional Concepts and Generics