Travis Griggs Travis Griggs - 6 months ago 28
Swift Question

BSON for Swift?

My question's pretty open ended at this point, but I'm curious if anyone has implemented something like SwiftyJSON for BSON in Swift?

I found that the Perfect project has something, but it seems to just be a wrapper around an existing C library (won't do me any good on the iOS side). I toyed with just porting/warping SwiftyJSON, the internals of which are a little above my learning curve so far AND it seems to just be a wrapper around the platform's

JSONSerialization
.

So is anyone either

A) aware of something done that my googling hasn't discovered yet or

B) or could help nudge me in the right direction with how to generally architect such a thing? (not trying to get others to do work for me)

aside: to forestall the "why don't you just use json" questions... it's because I'm doing quite a bit of MongoDB on the other side AND I work with a lot of Strings and Dates, which have to be ambiguously represented in JSON

Answer

In the interest of closure... I ended up writing my own. It is not a complete solution to all of the BSON encodings, just the ones that I'm using. It was fun to figure out how to do it with Swift Enums.

import Foundation

extension GeneratorType {
    mutating func next(n: Int) -> [Element] {
        var result: [Element] = []
        for _ in 1...n {
            if let next = self.next() {
                result.append(next)
            } else {
                break
            }
        }
        return result
    }
}

extension GeneratorType where Element:Comparable {
    mutating func upTo(match:Element) -> [Element]? {
        var result: [Element] = []
        while let next = self.next() {
            if next == match {
                return result
            }
            else {
                result.append(next)
            }
        }
        return nil
    }
}

extension String {
    init?<S : SequenceType, C : UnicodeCodecType where S.Generator.Element == C.CodeUnit>
        (codeUnits : S, inout codec : C) {
            var str = ""
            var generator = codeUnits.generate()
            var done = false
            while !done {
                let r = codec.decode(&generator)
                switch (r) {
                case .EmptyInput:
                    done = true
                case let .Result(val):
                    str.append(Character(val))
                case .Error:
                    return nil
                }
            }
            self = str
    }
}

enum BSON {
    static func toByteArray<T>(value: T) -> [UInt8] {
        var io = value
        return withUnsafePointer(&io) {
            Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>($0), count: sizeof(T)))
        }
    }

    static func fromByteArray<T>(value: [UInt8], _: T.Type) -> T {
        return value.withUnsafeBufferPointer {
            return UnsafePointer<T>($0.baseAddress).memory
        }
    }

    struct Field {
        var name:String
        var element:BSON
    }

    case double(Double)
    case string(String)
    case document([Field])
    case array([BSON])
    case binary([UInt8])
//  case objectid((UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8))
    case boolean(Bool)
    case datetime(NSDate)
    case null
    case int32(Int32)
    case int64(Int64)

    init() {
        self = .document([])
    }

    var bsonType:UInt8 {
        switch self {
        case .double: return 0x01
        case .string: return 0x02
        case .document: return 0x03
        case .array: return 0x04
        case .binary: return 0x05
//      case .objectid: return 0x07
        case .boolean: return 0x08
        case .datetime: return 0x09
        case .null: return 0x0A
        case .int32: return 0x10
        case .int64: return 0x12
        }
    }

    subscript(key:String) -> BSON {
        get {
            switch self {
            case .document (let fields):
                for field in fields {
                    if field.name == key {
                        return field.element
                    }
                }
                return BSON.null
            default:
                return BSON.null
            }
        }
        set(newValue) {
            var newFields:[Field] = []
            switch self {
            case .document (let fields):
                newFields = fields
                var append = true
                for (index, field) in newFields.enumerate() {
                    if field.name == key {
                        newFields[index].element = newValue
                        append = false
                        break
                    }
                }
                if append {
                    newFields.append(Field(name: key, element: newValue))
                }
            default:
                newFields = [Field(name: key, element: newValue)]
            }
            self = .document(newFields)
        }
    }


    subscript(index:Int) -> BSON {
        get {
            switch self {
            case .array (let elements):
                return index < elements.count ? elements[index] : BSON.null
            default:
                return BSON.null
            }
        }
        set(newValue) {
            switch self {
            case .array (let elements):
                if index < elements.count {
                    var newElements = elements
                    newElements[index] = newValue
                    self = .array(newElements)
                }
            default:
                break
            }
        }
    }

    func encoded() -> [UInt8] {
        switch self {
        case double (let value):
            return BSON.toByteArray(value)
        case string (let value):
            let encoded = value.utf8
            return BSON.toByteArray(Int32(encoded.count + 1)) + encoded + [0]
        case document (let fields):
            var body:[UInt8] = []
            for field in fields ?? [] {
                body += [field.element.bsonType]
                body += field.name.utf8
                body += [0]
                body += field.element.encoded()
            }
            body += [0]
            return BSON.toByteArray(Int32(body.count + 4)) + body
        case array (let elements):
            var body:[UInt8] = []
            for (index, element) in elements.enumerate() {
                body += [element.bsonType]
                body += String(index).utf8
                body += [0]
                body += element.encoded()
            }
            body += [0]
            return BSON.toByteArray(Int32(body.count + 4)) + body
        case binary (let bytes):
            return BSON.toByteArray(Int32(bytes.count)) + [0x00] + bytes
//      case objectid:
//          return []
        case boolean (let value):
            return value ? [0x01] : [0x00]
        case datetime (let value):
            let since = Int64(value.timeIntervalSince1970 * 1000.0)
            return BSON.toByteArray(since)
        case null:
            return []
        case int32 (let value):
            return BSON.toByteArray(value)
        case int64 (let value):
            return BSON.toByteArray(value)
        }
    }

    static func decode(inout stream stream:IndexingGenerator<[UInt8]>, bsonType:UInt8 = 0x03) -> BSON {
        switch bsonType {
        case 0x01:
            let bytes = stream.next(sizeof(Double))
            return self.double(fromByteArray(bytes, Double.self))
        case 0x02:
            let _ = stream.next(sizeof(Int32)) // skip the count
            if let buffer = stream.upTo(0) {
                var codec = UTF8()
                if let decoded = String(codeUnits: buffer, codec: &codec) {
                    return self.string(decoded)
                }
            }
            fatalError("utf8 parse error!")
        case 0x03:
            var fields:[Field] = []
            stream.next(sizeof(Int32)) // throw out size
            while let bsonType = stream.next() {
                if bsonType == 0 {
                    return self.document(fields)
                }
                if let buffer = stream.upTo(0) {
                    var codec = UTF8()
                    if let fieldName = String(codeUnits: buffer, codec: &codec) {
                        let element = BSON.decode(stream:&stream, bsonType: bsonType)
                        fields.append(Field(name: fieldName, element: element))
                    }
                }
            }
        case 0x04:
            var elements:[BSON] = []
            stream.next(sizeof(Int32)) // throw out size
            while let bsonType = stream.next() {
                if bsonType == 0 {
                    return self.array(elements)
                }
                stream.upTo(0) // skip name
                elements.append(BSON.decode(stream:&stream, bsonType: bsonType))
            }
        case 0x05:
            let count = fromByteArray(stream.next(sizeof(Int32)), Int32.self)
            assert(stream.next() == 0x00)
            return self.binary(stream.next(Int(count)))
        case 0x07:
            break
        case 0x08:
            let value = stream.next()
            return self.boolean(value == 0x01)
        case 0x09:
            let milliseconds = fromByteArray(stream.next(sizeof(Int64)), Int64.self)
            let interval = NSTimeInterval(milliseconds) / 1000.0
            return self.datetime(NSDate(timeIntervalSince1970: interval))
        case 0x0A:
            return self.null
        case 0x10:
            let value = fromByteArray(stream.next(sizeof(Int32)), Int32.self)
            return self.int32(value)
        case 0x12:
            let value = fromByteArray(stream.next(sizeof(Int64)), Int64.self)
            return self.int64(value)
        default:
            break
        }
        fatalError()
    }

    var document:BSON? {
        switch self {
        case .document:
            return self
        default:
            return nil
        }
    }

    var double:Double? {
        switch self {
        case .double (let value):
            return value
        default:
            return nil
        }
    }

    var int32:Int32? {
        get {
            switch self {
            case .int32 (let value):
                return value
            default:
                return nil
            }
        }
        set {
            if let newValue = newValue {
                self = .int32(newValue)
            }
            else {
                self = .null
            }
        }
    }


    var int64:Int64? {
        switch self {
        case .int64 (let value):
            return value
        default:
            return nil
        }
    }

    var boolean:Bool? {
        switch self {
        case .boolean (let value):
            return value
        default:
            return nil
        }
    }

    var binary:[UInt8]? {
        switch self {
        case .binary (let value):
            return value
        default:
            return nil
        }
    }

    var string:String? {
        switch self {
        case .string (let value):
            return value
        default:
            return nil
        }
    }

    var datetime:NSDate? {
        switch self {
        case .datetime (let value):
            return value
        default:
            return nil
        }
    }

    var array:[BSON]? {
        switch self {
        case .array (let elements):
            return elements
        default:
            return nil
        }
    }

    var isNull:Bool {
        switch self {
        case .null:
            return true
        default:
            return false
        }
    }

    var keyValues:[(String, BSON)] {
        switch self {
        case document (let fields):
            return fields.map() { ($0.name, $0.element) }
        default:
            return []
        }
    }

}

extension BSON: Equatable {}
extension BSON.Field: Equatable {}

func == (a:BSON.Field, b:BSON.Field) -> Bool {
    return a.name == b.name && a.element == b.element
}

func == (a:BSON, b:BSON) -> Bool {
    switch (a, b) {
    case (.double(let a),   .double(let b))   where a == b:             return true
    case (.string(let a),   .string(let b))   where a == b:             return true
    case (.document(let a), .document(let b)) where a == b:             return true
    case (.array(let a),    .array(let b))    where a == b:             return true
    case (.binary(let a),   .binary(let b))   where a == b:             return true
    case (.boolean(let a),  .boolean(let b))  where a == b:             return true
    case (.datetime(let a), .datetime(let b)) where a.isEqualToDate(b): return true
    case (.null,            .null):                                     return true
    case (.int32(let a),    .int32(let b))    where a == b:             return true
    case (.int64(let a),    .int64(let b))    where a == b:             return true
    default: return false
    }
}

protocol BSONConvertable {
    var bson:BSON { get }
}

extension Int32: BSONConvertable {
    var bson:BSON {
        return BSON.int32(self)
    }
}

extension Int64: BSONConvertable {
    var bson:BSON {
        return BSON.int64(self)
    }
}

extension Int: BSONConvertable {
    var bson:BSON {
        let wide = self.toIntMax()
        if Int32.min.toIntMax() <= wide && wide <= Int32.max.toIntMax() {
            return BSON.int32(Int32(wide))
        }
        else {
            return BSON.int64(Int64(wide))
        }
    }
}


extension Float:BSONConvertable {
    var bson:BSON {
        return Double(self).bson
    }
}

extension Double:BSONConvertable {
    var bson:BSON {
        return BSON.double(self)
    }
}

extension Bool:BSONConvertable {
    var bson:BSON {
        return BSON.boolean(self)
    }
}

extension BSON:BSONConvertable {
    var bson:BSON {
        return self
    }
}

extension NSDate:BSONConvertable {
    var bson:BSON {
        return BSON.datetime(self)
    }
}

extension String:BSONConvertable {
    var bson:BSON {
        return BSON.string(self)
    }
}

extension Array where Element:BSONConvertable {
    var bson:BSON {
        return BSON.array(self.map({$0.bson}))
    }
}
Comments