Skip to content

Instantly share code, notes, and snippets.

@curtclifton
Created February 8, 2015 23:10
Show Gist options
  • Save curtclifton/6ff5ea9d1169a249456d to your computer and use it in GitHub Desktop.
Save curtclifton/6ff5ea9d1169a249456d to your computer and use it in GitHub Desktop.
Hacking on a type-safe JSON library
//
// 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)")
}
}
}
@curtclifton
Copy link
Author

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:

    func testParseInterestingRecord() {
        var outCondition = true
        var outArray = [true]

        let requiredElements: [JSONRecordElement] = [
            JSONRecordElementOf<Bool>(key: "condition", shape: JSONBoolShape(), block: { outCondition = $0}),
            JSONRecordElementOf<[Bool]>(key: "array", shape: JSONArrayShape(JSONBoolShape()), block: { outArray = $0}),
        ]
        let recordShape: JSONRecordShape<Example> = JSONRecordShape(requiredElements: requiredElements) {
            return Example(condition: outCondition, array: outArray)
        }

        let goodRecord = [ "condition": NSNumber(bool: false), "array": [NSNumber(bool: false), NSNumber(bool: true)] ]
        expectSuccess(recordShape.parse(goodRecord), expectedValue: Example(condition: false, array: [false, true]))
    }

where the Example data type is defined like so:

struct Example: Equatable {
    let condition: Bool
    let array: [Bool]
}

func ==(lhs: Example, rhs: Example) -> Bool {
    return (lhs.condition == rhs.condition) && (lhs.array == rhs.array)
}

I'm using side-effecting blocks to capture values so Example that can be immutable. In practice the interesting code in testParseInterestingRecord would eventually be in a static method on a JSONDecodable extension of Example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment