Skip to content

Instantly share code, notes, and snippets.

@jarsen
Last active August 29, 2015 14:13
Show Gist options
  • Save jarsen/55ec8a9f378a05b2a199 to your computer and use it in GitHub Desktop.
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
// 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)
}
@jarsen
Copy link
Author

jarsen commented Jan 8, 2015

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.

@jarsen
Copy link
Author

jarsen commented Jan 8, 2015

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