Created
January 26, 2016 20:55
-
-
Save kjessup/dee0b1d79bdb6e9bacdb to your computer and use it in GitHub Desktop.
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
// | |
// JSONConvertable.swift | |
// | |
// Created by Kyle Jessup on 2016-01-21. | |
// Copyright © 2016 Treefrog. All rights reserved. | |
// | |
// This program is free software: you can redistribute it and/or modify | |
// it under the terms of the GNU Affero General Public License as | |
// published by the Free Software Foundation, either version 3 of the | |
// License, or (at your option) any later version, as supplemented by the | |
// Perfect Additional Terms. | |
// | |
// This program is distributed in the hope that it will be useful, | |
// but WITHOUT ANY WARRANTY; without even the implied warranty of | |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
// GNU Affero General Public License, as supplemented by the | |
// Perfect Additional Terms, for more details. | |
// | |
// You should have received a copy of the GNU Affero General Public License | |
// and the Perfect Additional Terms that immediately follow the terms and | |
// conditions of the GNU Affero General Public License along with this | |
// program. If not, see <http://www.perfect.org/AGPL_3_0_With_Perfect_Additional_Terms.txt>. | |
// | |
public class JSONDecoding { | |
private init() {} | |
public static let objectIdentifierKey = "_jsonobjid" | |
public typealias JSONConvertableObjectCreator = () -> JSONConvertableObject | |
static private var jsonDecodableRegistry = [String:JSONConvertableObjectCreator]() | |
static public func registerJSONDecodable(name: String, creator: JSONConvertableObjectCreator) { | |
JSONDecoding.jsonDecodableRegistry[name] = creator | |
} | |
static public func createJSONConvertableObject(values:[String:Any]) -> JSONConvertableObject? { | |
guard let objkey = values[JSONDecoding.objectIdentifierKey] as? String else { | |
return nil | |
} | |
return JSONDecoding.createJSONConvertableObject(objkey, values: values) | |
} | |
static public func createJSONConvertableObject(name: String, values:[String:Any]) -> JSONConvertableObject? { | |
guard let creator = JSONDecoding.jsonDecodableRegistry[name] else { | |
return nil | |
} | |
let jsonObj = creator() | |
jsonObj.setJSONValues(values) | |
return jsonObj | |
} | |
} | |
public protocol JSONConvertable { | |
func jsonValueString() throws -> String | |
} | |
public protocol JSONConvertableObject: JSONConvertable { | |
func setJSONValues(values:[String:Any]) | |
} | |
public extension JSONConvertableObject { | |
func getJSONValue<T: JSONConvertable>(named: String, from:[String:Any], defaultValue: T) -> T { | |
let f = from[named] | |
if let v = f as? T { | |
return v | |
} | |
return defaultValue | |
} | |
} | |
public enum JSONConversionError: ErrorType { | |
case NotConvertable(Any) | |
case InvalidKey(Any) | |
case SyntaxError | |
} | |
private let jsonBackSlash = UnicodeScalar(UInt32(92)) | |
private let jsonBackSpace = UnicodeScalar(UInt32(8)) | |
private let jsonFormFeed = UnicodeScalar(UInt32(12)) | |
private let jsonLF = UnicodeScalar(UInt32(10)) | |
private let jsonCR = UnicodeScalar(UInt32(13)) | |
private let jsonTab = UnicodeScalar(UInt32(9)) | |
private let jsonQuoteDouble = UnicodeScalar(UInt32(34)) | |
private let jsonOpenObject = UnicodeScalar(UInt32(123)) | |
private let jsonOpenArray = UnicodeScalar(UInt32(91)) | |
private let jsonCloseObject = UnicodeScalar(UInt32(125)) | |
private let jsonCloseArray = UnicodeScalar(UInt32(93)) | |
private let jsonWhiteSpace = UnicodeScalar(UInt32(32)) | |
private let jsonColon = UnicodeScalar(UInt32(58)) | |
private let jsonComma = UnicodeScalar(UInt32(44)) | |
extension String: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
var s = "\"" | |
for uchar in self.unicodeScalars { | |
switch(uchar) { | |
case jsonBackSlash: | |
s.appendContentsOf("\\\\") | |
case jsonQuoteDouble: | |
s.appendContentsOf("\\\"") | |
case jsonBackSpace: | |
s.appendContentsOf("\\b") | |
case jsonFormFeed: | |
s.appendContentsOf("\\f") | |
case jsonLF: | |
s.appendContentsOf("\\n") | |
case jsonCR: | |
s.appendContentsOf("\\r") | |
case jsonTab: | |
s.appendContentsOf("\\t") | |
default: | |
s.append(uchar) | |
} | |
} | |
s.appendContentsOf("\"") | |
return s | |
} | |
} | |
extension Int: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
return String(self) | |
} | |
} | |
extension Double: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
return String(self) | |
} | |
} | |
extension Optional: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
if self == nil { | |
return "null" | |
} else if let v = self! as? JSONConvertable { | |
return try v.jsonValueString() | |
} | |
throw JSONConversionError.NotConvertable(self) | |
} | |
} | |
extension Bool: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
if true == self { | |
return "true" | |
} | |
return "false" | |
} | |
} | |
extension Array: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
var s = "[" | |
var first = true | |
for v in self { | |
if !first { | |
s.appendContentsOf(",") | |
} else { | |
first = false | |
} | |
guard let jsonAble = v as? JSONConvertable else { | |
throw JSONConversionError.NotConvertable(v) | |
} | |
s.appendContentsOf(try jsonAble.jsonValueString()) | |
} | |
s.appendContentsOf("]") | |
return s | |
} | |
} | |
extension Dictionary: JSONConvertable { | |
public func jsonValueString() throws -> String { | |
var s = "{" | |
var first = true | |
for (k, v) in self { | |
guard let strKey = k as? String else { | |
throw JSONConversionError.InvalidKey(k) | |
} | |
guard let jsonAble = v as? JSONConvertable else { | |
throw JSONConversionError.NotConvertable(v) | |
} | |
if !first { | |
s.appendContentsOf(",") | |
} else { | |
first = false | |
} | |
s.appendContentsOf(try strKey.jsonValueString()) | |
s.appendContentsOf(":") | |
s.appendContentsOf(try jsonAble.jsonValueString()) | |
} | |
s.appendContentsOf("}") | |
return s | |
} | |
} | |
extension String { | |
public func jsonDecode() throws -> JSONConvertable? { | |
let state = JSONDecodeState() | |
state.g = self.unicodeScalars.generate() | |
let o = try state.readObject() | |
if let _ = o as? JSONDecodeState.EOF { | |
throw JSONConversionError.SyntaxError | |
} | |
return o | |
} | |
} | |
private class JSONDecodeState { | |
struct EOF: JSONConvertable { | |
func jsonValueString() throws -> String { return "" } | |
} | |
var g = String().unicodeScalars.generate() | |
var pushBack: UnicodeScalar? | |
func movePastWhite() { | |
while let c = self.next() { | |
if !c.isWhiteSpace() { | |
self.pushBack = c | |
break | |
} | |
} | |
} | |
func readObject() throws -> JSONConvertable? { | |
self.movePastWhite() | |
guard let c = self.next() else { | |
return EOF() | |
} | |
switch(c) { | |
case jsonOpenArray: | |
var a = [Any]() | |
self.movePastWhite() | |
guard let c = self.next() else { | |
throw JSONConversionError.SyntaxError | |
} | |
if c != jsonCloseArray { | |
self.pushBack = c | |
while true { | |
a.append(try readObject()) | |
self.movePastWhite() | |
guard let c = self.next() else { | |
throw JSONConversionError.SyntaxError | |
} | |
if c == jsonCloseArray { | |
break | |
} | |
if c != jsonComma { | |
throw JSONConversionError.SyntaxError | |
} | |
} | |
} | |
return a | |
case jsonOpenObject: | |
var d = [String:Any]() | |
self.movePastWhite() | |
guard let c = self.next() else { | |
throw JSONConversionError.SyntaxError | |
} | |
if c != jsonCloseObject { | |
self.pushBack = c | |
while true { | |
guard let key = try readObject() as? String else { | |
throw JSONConversionError.SyntaxError | |
} | |
self.movePastWhite() | |
guard let c = self.next() else { | |
throw JSONConversionError.SyntaxError | |
} | |
guard c == jsonColon else { | |
throw JSONConversionError.SyntaxError | |
} | |
self.movePastWhite() | |
d[key] = try readObject() | |
do { | |
guard let c = self.next() else { | |
throw JSONConversionError.SyntaxError | |
} | |
if c == jsonCloseObject { | |
break | |
} | |
if c != jsonComma { | |
throw JSONConversionError.SyntaxError | |
} | |
} | |
} | |
} | |
if let objid = d[JSONDecoding.objectIdentifierKey] as? String { | |
if let o = JSONDecoding.createJSONConvertableObject(objid, values: d) { | |
return o | |
} | |
} | |
return d | |
case jsonQuoteDouble: | |
return try readString() | |
default: | |
if c.isWhiteSpace() { | |
// nothing | |
} else if c.isDigit() || c == "-" || c == "+" { | |
return try readNumber(c) | |
} else if c == "t" || c == "T" { | |
return try readTrue() | |
} else if c == "f" || c == "F" { | |
return try readFalse() | |
} else if c == "n" || c == "N" { | |
try readNull() | |
return nil as String? | |
} | |
} | |
throw JSONConversionError.SyntaxError | |
} | |
func next() -> UnicodeScalar? { | |
if pushBack != nil { | |
let c = pushBack! | |
pushBack = nil | |
return c | |
} | |
return g.next() | |
} | |
// the opening quote has been read | |
func readString() throws -> String { | |
var next = self.next() | |
var esc = false | |
var s = "" | |
while let c = next { | |
if esc { | |
switch(c) { | |
case jsonBackSlash: | |
s.append(jsonBackSlash) | |
case jsonQuoteDouble: | |
s.append(jsonQuoteDouble) | |
case "b": | |
s.append(jsonBackSpace) | |
case "f": | |
s.append(jsonFormFeed) | |
case "n": | |
s.append(jsonLF) | |
case "r": | |
s.append(jsonCR) | |
case "t": | |
s.append(jsonTab) | |
case "u": | |
var hexStr = "" | |
for _ in 1...4 { | |
next = self.next() | |
guard let hexC = next else { | |
throw JSONConversionError.SyntaxError | |
} | |
guard hexC.isHexDigit() else { | |
throw JSONConversionError.SyntaxError | |
} | |
hexStr.append(hexC) | |
} | |
let result = UnicodeScalar(UInt32(strtoul(hexStr, nil, 16))) | |
s.append(result) | |
default: | |
s.append(c) | |
} | |
esc = false | |
} else if c == jsonBackSlash { | |
esc = true | |
} else if c == jsonQuoteDouble { | |
return s | |
} else { | |
s.append(c) | |
} | |
next = self.next() | |
} | |
throw JSONConversionError.SyntaxError | |
} | |
func readNumber(firstChar: UnicodeScalar) throws -> JSONConvertable { | |
var s = "" | |
var needPeriod = true, needExp = true | |
s.append(firstChar) | |
if firstChar == "." { | |
needPeriod = false | |
} | |
var next = self.next() | |
var last = firstChar | |
while let c = next { | |
if c.isDigit() { | |
s.append(c) | |
} else if c == "." && !needPeriod { | |
break | |
} else if (c == "e" || c == "E") && !needExp { | |
break | |
} else if c == "." { | |
needPeriod = false | |
s.append(c) | |
} else if c == "e" || c == "E" { | |
needExp = false | |
s.append(c) | |
next = self.next() | |
if next != nil && (next! == "-" || next! == "+") { | |
s.append(next!) | |
} else { | |
pushBack = next! | |
} | |
} else if last.isDigit() { | |
pushBack = c | |
if needPeriod && needExp { | |
return Int(s)! | |
} | |
return Double(s)! | |
} else { | |
break | |
} | |
last = c | |
next = self.next() | |
} | |
throw JSONConversionError.SyntaxError | |
} | |
func readTrue() throws -> Bool { | |
var next = self.next() | |
if next != "r" && next != "R" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "u" && next != "U" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "e" && next != "E" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
guard next != nil && !next!.isAlphaNum() else { | |
throw JSONConversionError.SyntaxError | |
} | |
pushBack = next! | |
return true | |
} | |
func readFalse() throws -> Bool { | |
var next = self.next() | |
if next != "a" && next != "A" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "l" && next != "L" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "s" && next != "S" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "e" && next != "E" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
guard next != nil && !next!.isAlphaNum() else { | |
throw JSONConversionError.SyntaxError | |
} | |
pushBack = next! | |
return false | |
} | |
func readNull() throws { | |
var next = self.next() | |
if next != "u" && next != "U" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "l" && next != "L" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
if next != "l" && next != "L" { | |
throw JSONConversionError.SyntaxError | |
} | |
next = self.next() | |
guard next != nil && !next!.isAlphaNum() else { | |
throw JSONConversionError.SyntaxError | |
} | |
pushBack = next! | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment