Created
February 8, 2015 23:10
-
-
Save curtclifton/6ff5ea9d1169a249456d to your computer and use it in GitHub Desktop.
Hacking on a type-safe JSON library
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
// | |
// JSONShape.swift | |
// TypedJSON | |
// | |
// Created by Curt Clifton on 2/7/15. | |
// Copyright (c) 2015 curtclifton.net. All rights reserved. | |
// | |
import Foundation | |
// CCC, 2/7/2015. not sure what I want to do for errors yet: | |
public enum JSONError { | |
case ParseError(String) | |
} | |
// hateful wrapper to avoid the dreaded “unimplemented IR generation feature non-fixed multi-payload enum layout” | |
public struct Box<T> { | |
private let boxedValue: [T] | |
public init(_ value: T) { | |
boxedValue = [value] | |
} | |
public var value: T { | |
return boxedValue.first! | |
} | |
} | |
public enum Result<T> { | |
case Value(Box<T>) | |
case Error(JSONError) | |
} | |
extension Array { | |
func map<U>(f: (T) -> Result<U>) -> Result<[U]> { | |
return self.reduce(Result.Value(Box([U]()))) { accum, element in | |
// CCC, 2/8/2015. can probably use bind here | |
switch accum { | |
case .Error(let error): | |
return Result.Error(error) | |
case .Value(let accumBox): | |
var arrayThusFar = accumBox.value | |
switch f(element) { | |
case .Value(let elementBox): | |
arrayThusFar.append(elementBox.value) | |
return Result.Value(Box(arrayThusFar)) | |
case .Error(let error): | |
return Result.Error(error) | |
} | |
} | |
} | |
} | |
} | |
public protocol JSONShape { | |
typealias ResultType | |
func parse(jsonObject: AnyObject) -> Result<ResultType> | |
} | |
public struct JSONBoolShape: JSONShape { | |
typealias ResultType = Bool | |
public init() { | |
// nothing to do here | |
} | |
public func parse(jsonObject: AnyObject) -> Result<Bool> { | |
if let boolNumber = jsonObject as? NSNumber { | |
return Result.Value(Box(boolNumber.boolValue)) | |
} | |
return Result.Error(JSONError.ParseError("Unable to parse \(jsonObject) as Bool")) | |
} | |
} | |
// CCC, 2/7/2015. JSONIntShape | |
// CCC, 2/7/2015. JSONDoubleShape | |
// CCC, 2/7/2015. JSONStringShape | |
// CCC, 2/7/2015. JSONNullShape??? | |
public struct JSONArrayShape<U>: JSONShape { | |
typealias ResultType = [U] | |
let elementParserFunction: AnyObject -> Result<U> | |
public init<S: JSONShape where S.ResultType == U>(_ elementShape: S) { | |
// Capturing the parsing function as a closure so that the important type constraint on S, that its result type is U, can be captured outside the scope of S. Thanks to the answer on this post for the technique: http://stackoverflow.com/questions/26832498/how-is-a-type-erased-generic-wrapper-implemented | |
elementParserFunction = { jsonElement in elementShape.parse(jsonElement) } | |
} | |
public func parse(jsonObject: AnyObject) -> Result<[U]> { | |
if let array = jsonObject as? [AnyObject] { | |
return array.map(elementParserFunction) | |
} | |
return Result.Error(JSONError.ParseError("Unable to parse \(jsonObject) as array")) | |
} | |
} | |
public struct JSONRecordShape<U>: JSONShape { | |
typealias ResultType = U | |
let requiredElements: [JSONRecordElement] | |
let builder: () -> U | |
public init(requiredElements: [JSONRecordElement], builder: () -> U) { | |
self.requiredElements = requiredElements | |
self.builder = builder | |
} | |
public func parse(jsonObject: AnyObject) -> Result<U> { | |
if let dictionary = jsonObject as? Dictionary<String, AnyObject> { | |
// CCC, 2/8/2015. need to iterate over the expected keys | |
let mapResult: Result<[Bool]> = requiredElements.map { recordElement in | |
if let error = recordElement.process(dictionary) { | |
return Result.Error(error) | |
} else { | |
return Result.Value(Box(true)) // a bit weird, but in the success case we just process elements for their side effects | |
} | |
} | |
// CCC, 2/8/2015. use a functional bind operator | |
switch mapResult { | |
case .Error(let error): | |
return Result.Error(error) | |
case .Value(let trueBox): | |
return Result.Value(Box(builder())) | |
} | |
} | |
return Result.Error(JSONError.ParseError("Unable to parse \(jsonObject) as record. Expected a dictionary.")) | |
} | |
} | |
public protocol JSONRecordElement { | |
func process(dictionary: Dictionary<String, AnyObject>) -> JSONError? | |
} | |
public struct JSONRecordElementOf<T>: JSONRecordElement { | |
let key: String | |
let parser: (AnyObject) -> Result<T> | |
let block: (T) -> Void | |
public init<S: JSONShape where S.ResultType == T>(key: String, shape: S, block: (T) -> Void) { | |
self.key = key | |
self.parser = { jsonObject in shape.parse(jsonObject) } | |
self.block = block | |
} | |
public func process(dictionary: Dictionary<String, AnyObject>) -> JSONError? { | |
// CCC, 2/8/2015. bind? | |
if let jsonObject: AnyObject = dictionary[key] { | |
switch parser(jsonObject) { | |
case .Value(let resultBox): | |
block(resultBox.value) | |
return nil | |
case .Error(let error): | |
return error | |
} | |
} else { | |
return JSONError.ParseError("Unable to parse \(dictionary). Missing key \(key)") | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There's still a lot to do, but the basic idea is to allow types to declaratively specify their shape as JSON, then have the library handle parsing and encoding. I'm working on the parsing bit now, as it seems harder. Here's an example test case for JSONRecordShape:
where the Example data type is defined like so:
I'm using side-effecting blocks to capture values so
Example
that can be immutable. In practice the interesting code intestParseInterestingRecord
would eventually be in a static method on a JSONDecodable extension ofExample
.