Last active
August 29, 2015 14:13
-
-
Save jarsen/55ec8a9f378a05b2a199 to your computer and use it in GitHub Desktop.
Parsing JSON Playground with Result type. Inspired by Haskell, Swiftz, and http://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics
This file contains hidden or 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
// Playground - noun: a place where people can play | |
import Foundation | |
infix operator >>- { associativity left precedence 150 } // Bind | |
infix operator <^> { associativity left } // Functor's fmap (usually <$>) | |
infix operator <*> { associativity left } // Applicative's apply | |
public typealias JSON = AnyObject | |
public typealias JSONObject = Dictionary<String, JSON> | |
public typealias JSONArray = Array<JSON> | |
public protocol JSONDecoding { | |
class func decode(json: JSONObject) -> Result<Self> | |
} | |
func JSONString(json: JSONObject)(key: String) -> Result<String> { | |
if let string = json[key] as? String { | |
return .Success(string) | |
} | |
else { | |
return .Failure("json did not have String for key: \(key)") | |
} | |
} | |
func JSONOptionalString(json: JSONObject)(key: String) -> Result<String?> { | |
if let string = json[key] as? String { | |
return .Success(string) | |
} | |
else { | |
return .Success(nil) | |
} | |
} | |
func JSONInt(json: JSONObject)(key: String) -> Result<Int> { | |
if let int = json[key] as? Int { | |
return .Success(int) | |
} | |
else { | |
return .Failure("json did not have Int for key: \(key)") | |
} | |
} | |
func JSONOptionalInt(json: JSONObject)(key: String) -> Result<Int?> { | |
if let int = json[key] as? Int { | |
return .Success(int) | |
} | |
else { | |
return .Success(nil) | |
} | |
} | |
func JSONURL(json: JSONObject)(key: String) -> Result<NSURL> { | |
if let urlString = json[key] as? String { | |
if let url = NSURL(string: urlString) { | |
return .Success(url) | |
} | |
else { | |
return .Failure("json object has invalid URL: \(urlString)") | |
} | |
} | |
else { | |
return .Failure("json did not have URL for key: \(key)") | |
} | |
} | |
func JSONBool(json: JSONObject)(key: String) -> Result<Bool> { | |
if let bool = json[key] as? Bool { | |
return .Success(bool) | |
} | |
else { | |
return .Failure("json did not have Bool for key: \(key)") | |
} | |
} | |
public enum Result<T> { | |
case Success(@autoclosure () -> T) | |
case Failure(String) | |
init(_ value:T) { | |
self = .Success(value) | |
} | |
} | |
extension Result { | |
public func bind<U>(f: T -> Result<U>) -> Result<U> { | |
switch self { | |
case let .Success(value): | |
return f(value()) | |
case let .Failure(error): | |
return .Failure(error) | |
} | |
} | |
public func fmap<U>(f: T->U) -> Result<U> { | |
switch self { | |
case let .Success(value): | |
return .Success(f(value())) | |
case let .Failure(error): | |
return .Failure(error) | |
} | |
} | |
public func forceUnwrap() -> T { | |
switch self { | |
case let .Success(value): | |
return value() | |
case let .Failure: | |
assertionFailure("You force unwrapped \(self), which was not a Result.Success") | |
} | |
} | |
} | |
// MARK: Fancy Operators | |
func >>-<A,B>(lhs: A->Result<B>, rhs: Result<A>) -> Result<B> { | |
return rhs.bind(lhs) | |
} | |
func <^><A,B>(lhs: A->B, rhs: Result<A>) -> Result<B> { | |
return rhs.fmap(lhs) | |
} | |
func apply<A,B>(x: Result<A>, f: Result<(A->B)>) -> Result<B> { | |
return x.bind { xValue in | |
f.bind { fx in | |
return .Success(fx(xValue)) | |
} | |
} | |
} | |
func <*><A,B>(lhs: Result<A->B>, rhs: Result<A>) -> Result<B> { | |
return apply(rhs, lhs) | |
} | |
// MARK: Example Data Type | |
struct User : JSONDecoding { | |
var id: Int | |
var email: String | |
var name: String | |
var nickname: String? | |
var blog: NSURL | |
static func create(id: Int)(email: String)(name: String)(nickname: String?)(blog: NSURL) -> User { | |
return User(id: id, email: email, name: name, nickname: nickname, blog: blog) | |
} | |
static func decode(json: JSONObject) -> Result<User> { | |
return User.create <^> | |
JSONInt(json)(key: "id") <*> | |
JSONString(json)(key: "email") <*> | |
JSONString(json)(key: "name") <*> | |
JSONOptionalString(json)(key: "nickname") <*> | |
JSONURL(json)(key: "website") | |
} | |
} | |
// MARK: Playing Around | |
let json = ["id": 2, "email": "[email protected]", "name": "Jason", "website": "http://example.com", "nickname":"jarsen"] | |
let successResult = User.decode(json) | |
switch successResult { | |
case let .Success(v): | |
let value = v() | |
case let .Failure(error): | |
println(error) | |
} | |
let wrongJSON = ["id": 2, "e": "[email protected]", "name": "Jason", "website": "http://example.com"] | |
var failResult = User.decode(wrongJSON) | |
switch failResult { | |
case let .Success(v): | |
let value = v() | |
case let .Failure(error): | |
println(error) | |
} |
I also added in a little bit for dealing with optional values/NSNull in the JSON. And URL handlers. You can imagine how you might easily extend this to parse dates, and other data types.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The cool thing about this style as compared to the thoughtbot article is that you get specific error messages when a key is not found, fails to parse, etc.