Skip to content

Instantly share code, notes, and snippets.

@kjessup
Created January 26, 2016 20:55
Show Gist options
  • Save kjessup/dee0b1d79bdb6e9bacdb to your computer and use it in GitHub Desktop.
Save kjessup/dee0b1d79bdb6e9bacdb to your computer and use it in GitHub Desktop.
//
// 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