Created
January 2, 2016 20:59
-
-
Save op183/92263a22a5b44e32d57d to your computer and use it in GitHub Desktop.
very simple JSON written in pure Swift
This file contains hidden or 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
// | |
// 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)}") | |
} | |
} | |
} | |
} |
This file contains hidden or 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
// | |
// 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