Skip to content

Instantly share code, notes, and snippets.

@op183
Created January 2, 2016 20:59
Show Gist options
  • Save op183/92263a22a5b44e32d57d to your computer and use it in GitHub Desktop.
Save op183/92263a22a5b44e32d57d to your computer and use it in GitHub Desktop.
very simple JSON written in pure Swift
//
// Json.swift
// BotWaRz
//
// Created by Ivo Vacek on 06/10/15.
// Copyright © 2015 Ivo Vacek. All rights reserved.
//
// enum doesn't support stored properties !!!
public enum Json: CustomDebugStringConvertible {
case NullValue
case BooleanValue(Bool)
case NumberValue(Double)
case StringValue(String)
case ArrayValue([Json])
case ObjectValue([String:Json])
// debugDescription is used by debugPrint()
// print() will still use writeTo()
// as defined by Streamable extension
public var debugDescription: String {
var indent: Int = 0
return prettyprint(&indent, json: self)
}
func prettyprint(inout indent: Int, json: Json)->String {
func space(indent: Int) -> String {
var space = ""
for _ in 0..<indent {
space.write("\t")
}
return space
}
var target = ""
switch json {
case .NullValue:
target.write("null")
case .BooleanValue(let b):
target.write("\(b)")
case .NumberValue(let d):
// if the last part is just .0, present the number as an integer
// in this case, the conversion from double to int is guaranteed
var s = "\(d)"
let c = "\(d)".characters.suffix(2).map{ $0 }
if c[0] == Character(".") && c[1] == Character("0") {
s = "\(Int(d))"
}
target.write(s)
case .StringValue(let s):
target.write("\"\(s)\"")
case .ArrayValue(let arr):
if arr.count == 0 {
target.write("[]")
} else {
var str = "[\n"
indent++
arr.forEach{
str.write(space(indent))
str.write(prettyprint(&indent, json: $0))
str.write(",\n")
}
let se = str.endIndex
str.removeAtIndex(se.advancedBy(-2))
str.write(space(--indent))
str.write("]")
target.write("\(str)")
}
case .ObjectValue(let dict):
if dict.count == 0 {
target.write("{}")
} else {
var str: String = "{\n"
indent++
dict.forEach { (key,value) -> () in
str.write(space(indent))
str.write(prettyprint(&indent, json: Json.from(key)))
str.write(": ")
str.write(prettyprint(&indent, json: value))
str.write(",\n")
}
let se = str.endIndex
str.removeAtIndex(se.advancedBy(-2))
str.write(space(--indent))
target.write("\(str)}")
}
}
return target
}
}
// MARK: - static functions
// useful for Json creation
extension Json {
static func from(value: Bool) -> Json {
return .BooleanValue(value)
}
static func from(value: Double) -> Json {
return .NumberValue(value)
}
static func from(value: String) -> Json {
return .StringValue(value)
}
static func from(value: [Json]) -> Json {
return .ArrayValue(value)
}
static func from(value: [String:Json]) -> Json {
return .ObjectValue(value)
}
}
// MARK: - computed variables
// useful for user object creation
extension Json {
public var isNull: Bool {
get {
switch self {
case .NullValue:
return true
default:
return false
}
}
}
public var isArray: Bool {
get {
switch self {
case .ArrayValue:
return true
default:
return false
}
}
}
public var isObject: Bool {
get {
switch self {
case .ObjectValue:
return true
default:
return false
}
}
}
public var boolValue: Bool? {
get {
switch self {
case .BooleanValue(let b):
return b
default:
return nil
}
}
}
public var doubleValue: Double? {
get {
switch self {
case .NumberValue(let n):
return n
default:
return nil
}
}
}
public var stringValue: String? {
get {
switch self {
case .StringValue(let s):
return s
default:
return nil
}
}
}
public var arrayValue: [Json] {
get {
switch self {
case .ArrayValue(let array):
return array
default:
return []
}
}
}
public var objectValue: [String:Json] {
get {
switch self {
case .ObjectValue(let dictionary):
return dictionary
default:
return [:]
}
}
}
public subscript(index: Int) -> Json {
get {
switch self {
case .ArrayValue(let array):
return index < array.count ? array[index] : .NullValue
default:
return .NullValue
}
}
}
public subscript(key: String) -> Json {
get {
switch self {
case .ObjectValue(let object):
return object[key] ?? .NullValue
default:
return .NullValue
}
}
}
}
// MARK: - Streamable
// writeTo is used also by print() and debugPrint() functions, except if
// we conform to CustomStringConveryible,
// resp. CustomDebugStringConvertible protocol
extension Json: Streamable {
public func writeTo<Target : OutputStreamType>(inout target: Target) {
switch self {
case .NullValue:
target.write("null")
case .BooleanValue(let b):
target.write("\(b)")
case .NumberValue(let d):
// if the last part is just .0, present the number as an integer
// in this case, the conversion from double to int is guaranteed
var s = "\(d)"
let c = "\(d)".characters.suffix(2).map{ $0 }
if c[0] == Character(".") && c[1] == Character("0") {
s = "\(Int(d))"
}
target.write(s)
case .StringValue(let s):
target.write("\"\(s)\"")
case .ArrayValue(let arr):
if arr.count == 0 {
target.write("[]")
} else {
var str = "["
arr.forEach{ (a) -> () in
//print(a, separator: "", terminator: ",", toStream: &str)
str.write("\(a),")
}
let se = str.endIndex
str.removeAtIndex(se.advancedBy(-1))
target.write("\(str)]")
}
case .ObjectValue(let dict):
if dict.count == 0 {
target.write("{}")
} else {
var str: String = "{"
dict.forEach { (key,value) -> () in
str.write("\"\(key)\":\(value),")
}
let se = str.endIndex
str.removeAtIndex(se.advancedBy(-1))
target.write("\(str)}")
}
}
}
}
//
// GenericJsonParser.swift
// BotWaRz
//
// Created by Ivo Vacek on 06/10/15.
// Copyright © 2015 Ivo Vacek. All rights reserved.
//
public enum JsonParserResult {
case Ok(Json)
case Error(String)
}
// Buffer is collection of UTF8.CodeUnit values ( currently UInt8, 8bits alias byte )
// to be easy using it with Swift build-in String (String.utf8) or with [byte] as reveived
// directly from socket
// WARNING !!
// processing of String.utf8 and Array<UF8.CodeUnit> aka [UInt8] 'slightly' differs
// need to work on better error handling if the missing or extra comma occures
// now it seems to be a hard work for no value at all .... :-)
public class JsonParser<Buffer: CollectionType where Buffer.Generator.Element == UTF8.CodeUnit> {
private let buffer: Buffer
private var cur: Buffer.Index
private let end: Buffer.Index
init(_ source: Buffer) {
buffer = source
cur = source.startIndex
end = source.endIndex
}
public func parse() -> JsonParserResult {
switch parseValue() {
case .Ok(let json):
return .Ok(json)
case .Error(let error):
return .Error(error)
}
}
// MARK: - private functions
// valid values are null | true | false | string | number | array | object
private func parseValue() -> JsonParserResult {
skipWhiteSpace()
if cur == end {
return .Error("end of buffer")
}
switch buffer[cur] {
case UTF8.CodeUnit(ascii: "n"):
return parseNull()
case UTF8.CodeUnit(ascii: "t"):
return parseTrue()
case UTF8.CodeUnit(ascii: "f"):
return parseFalse()
case UTF8.CodeUnit(ascii: "\""):
return parseString()
case let v where "-1234567890".utf8.contains(v):
return parseNumber()
case UTF8.CodeUnit(ascii: "["):
return parseArray()
case UTF8.CodeUnit(ascii: "{"):
return parseObject()
default:
return .Error("missing tag at position: \(buffer.count - cur.distanceTo(end))")
}
}
private func parseNull() -> JsonParserResult {
if parseTemplate("null") {
return .Ok(.NullValue)
} else {
return .Error("parse null at position: \(buffer.count - cur.distanceTo(end))")
}
}
private func parseTrue() -> JsonParserResult {
if parseTemplate("true") {
return .Ok(.BooleanValue(true))
} else {
return .Error("parse true at position: \(buffer.count - cur.distanceTo(end))")
}
}
private func parseFalse() -> JsonParserResult {
if parseTemplate("false") {
return .Ok(.BooleanValue(false))
} else {
return .Error("parse false at position: \(buffer.count - cur.distanceTo(end))")
}
}
private func parseString() -> JsonParserResult {
var endTag = cur
while ++endTag != end && buffer[endTag] != UTF8.CodeUnit(ascii: "\"") {}
if endTag == end {
return .Error("parse string at position: \(buffer.count - cur.distanceTo(end))")
}
let r = Range(start: ++cur, end: endTag)
cur = ++endTag
let subbuffer = buffer[r]
// in case the buffer is [UTF8.CodeUnit]
// ArraySlice should be converted to String value
if subbuffer is ArraySlice<UTF8.CodeUnit> {
let subbuffer2 = subbuffer as! ArraySlice<UTF8.CodeUnit>
let cstr = subbuffer2.map{ CChar(bitPattern: $0) } + [0]
return.Ok(.StringValue(String.fromCString(cstr)!))
} else {
return .Ok(.StringValue(String(subbuffer)))
}
}
private func parseNumber() -> JsonParserResult {
var endOfNumber = cur
let pos = cur
while ++endOfNumber != end && "+-.1234567890eE".utf8.contains(buffer[endOfNumber]){}
if endOfNumber == end {
return .Error("parse number at position: \(buffer.count - cur.distanceTo(end))")
}
let r = Range(start: cur, end: endOfNumber)
cur = endOfNumber
let subbuffer = buffer[r]
var number = Double()
// in case the buffer is [UTF8.CodeUnit]
// ArraySlice should be converted to String value
if subbuffer is ArraySlice<UTF8.CodeUnit> {
let subbuffer2 = subbuffer as! ArraySlice<UTF8.CodeUnit>
let cstr = subbuffer2.map{ CChar(bitPattern: $0) } + [0]
guard let d = Double(String.fromCString(cstr)!) else {
return .Error("invalid number at position: \(buffer.count - pos.distanceTo(end))")
}
number = d
} else {
guard let d = Double(String(subbuffer)) else {
return .Error("invalid number at position: \(buffer.count - pos.distanceTo(end))")
}
number = d
}
return .Ok(.NumberValue(number))
}
private func parseArray() -> JsonParserResult {
// Json array is comma separated list of Json values
let pos = cur
var arr: [Json] = []
// consume start tag
cur++
skipWhiteSpace()
// if end tag, return empty array
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: "]") {
// consume end tag
cur++
return .Ok(.ArrayValue(arr))
}
repeat {
switch parseValue() {
case .Error(let error):
return .Error(error)
case .Ok(let value):
arr.append(value)
}
skipWhiteSpace()
// consume value separator
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: ",") {
cur++
} else {
// missing comma should be parsed as an error
// for BotWaRz i just ignore this, because this error
// occures in example on the web
// simle
// return .Error("missing value separator at position: \(buffer.count - cur.distanceTo(end))")
// doesn't work for last value in object!!!
}
skipWhiteSpace()
// if end tag, just break from loop
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: "]") {
break
}
} while cur != end
if cur == end {
return .Error("error parse array at position: \(buffer.count - pos.distanceTo(end))")
} else {
// consume end tag
cur++
return .Ok(.ArrayValue(arr))
}
}
private func parseObject() -> JsonParserResult {
// Josn object is comma separated list of Json "key":value pairs
// the key must be a string !!!
let pos = cur
var object: [String:Json] = [:]
// consume start tag
cur++
skipWhiteSpace()
// if end tag, return empty object
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: "}") {
// consume end tag
cur++
return .Ok(.ObjectValue(object))
}
repeat {
let pos = cur
switch parseValue() {
case .Error(let error):
return .Error(error)
case .Ok(.StringValue(let key)):
// now expecting : as a separator between the key and the value
skipWhiteSpace()
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: ":") {
// consume it
cur++
} else {
return .Error("missing key value separator at position: \(buffer.count - cur.distanceTo(end))")
}
switch parseValue() {
case .Error(let error):
return .Error(error)
case .Ok(let value):
object.updateValue(value, forKey: key)
}
default:
return .Error("key must be a string at position: \(buffer.count - pos.distanceTo(end))")
}
skipWhiteSpace()
// consume value separator
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: ",") {
cur++
} else {
// missing comma should be parsed as an error
// for BotWaRz i just ignore this, because this error
// occures in examples on the web
}
skipWhiteSpace()
// if end tag, just break from loop
if cur != end && buffer[cur] == UTF8.CodeUnit(ascii: "}") {
break
}
} while cur != end
if cur == end {
return .Error("error parse odject at position: \(buffer.count - pos.distanceTo(end))")
}
// consume end tag
cur++
return .Ok(.ObjectValue(object))
}
// MARK: - helper functions
private func skipWhiteSpace() {
while cur != end && " \t\r\n".utf8.contains(buffer[cur]){ cur++ }
}
private func parseTemplate(template: String) -> Bool {
let templateBuffer = template.utf8
var c = templateBuffer.startIndex
let e = templateBuffer.endIndex
while c != e && cur != end {
if buffer[cur] == templateBuffer[c] {
c++
cur++
} else {
return false
}
}
if c == e {
return true
} else {
return false
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment