Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions Sources/SwiftBSON/ExtendedJSONDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ExtrasJSON
import Foundation
import NIO

/// `ExtendedJSONDecoder` facilitates the decoding of ExtendedJSON into `Decodable` values.
public class ExtendedJSONDecoder {
Expand Down Expand Up @@ -42,19 +43,13 @@ public class ExtendedJSONDecoder {
/// Initialize an `ExtendedJSONDecoder`.
public init() {}

/// Decodes an instance of the requested type `T` from the provided extended JSON data.
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - type: Codable type to decode the input into.
/// - data: `Data` which represents the JSON that will be decoded.
/// - Returns: Decoded representation of the JSON input as an instance of `T`.
/// - Throws: `DecodingError` if the JSON data is corrupt or if any value throws an error during decoding.
public func decode<T: Decodable>(_: T.Type, from data: Data) throws -> T {
private func decodeBytes<T: Decodable, C: Collection>(_: T.Type, from bytes: C) throws -> T
where C.Element == UInt8
{
// Data --> JSONValue --> BSON --> T
// Takes in JSON as `Data` encoded with `.utf8` and runs it through ExtrasJSON's parser to get an
// instance of the `JSONValue` enum.
let json = try JSONParser().parse(bytes: data)
let json = try JSONParser().parse(bytes: bytes)

// Then a `BSON` enum instance is decoded from the `JSONValue`.
let bson = try self.decodeBSONFromJSON(json, keyPath: [])
Expand All @@ -65,6 +60,38 @@ public class ExtendedJSONDecoder {
return try bsonDecoder.decode(T.self, fromBSON: bson)
}

/// Decodes an instance of the requested type `T` from the provided extended JSON data.
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - type: Codable type to decode the input into.
/// - data: `Data` which represents the JSON that will be decoded.
/// - Returns: Decoded representation of the JSON input as an instance of `T`.
/// - Throws: `DecodingError` if the JSON data is corrupt or if any value throws an error during decoding.
public func decode<T: Decodable>(_: T.Type, from data: Data) throws -> T {
try self.decodeBytes(T.self, from: data)
}

/// Decodes an instance of the requested type `T` from the provided extended JSON data.
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - type: Codable type to decode the input into.
/// - buffer: `ByteBuffer` which contains the JSON data that will be decoded.
/// - Returns: Decoded representation of the JSON input as an instance of `T`.
/// - Throws: `DecodingError` if the JSON data is corrupt or if any value throws an error during decoding.
public func decode<T: Decodable>(_: T.Type, from buffer: ByteBuffer) throws -> T {
guard buffer.readableBytes > 0 else {
throw DecodingError._extendedJSONError(keyPath: [], debugDescription: "empty buffer provided to decode")
}

var buffer = buffer
// readBytes never returns nil here because we checked that the buffer wasn't empty and only read
// readable bytes out from it.
// swiftlint:disable:next force_unwrapping
return try self.decodeBytes(T.self, from: buffer.readBytes(length: buffer.readableBytes)!)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to the swift-extras-json readme, it's more performant to read the bytes out of the buffer than it is to use ByteBufferView via readableBytesView. I think this is because the view relies on readInteger for each byte, which can be expensive to call repeatedly.

}

/// Decode a `BSON` from the given extended JSON.
private func decodeBSONFromJSON(_ json: JSONValue, keyPath: [String]) throws -> BSON {
switch try self.decodeScalar(json, keyPath: keyPath) {
Expand Down
41 changes: 30 additions & 11 deletions Sources/SwiftBSON/ExtendedJSONEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ExtrasJSON
import Foundation
import NIO

/// Facilitates the encoding of `Encodable` values into ExtendedJSON.
public class ExtendedJSONEncoder {
Expand All @@ -24,16 +25,7 @@ public class ExtendedJSONEncoder {
/// Initialize an `ExtendedJSONEncoder`.
public init() {}

/// Encodes an instance of the Encodable Type `T` into Data representing canonical or relaxed extended JSON.
/// The value of `self.mode` will determine which format is used. If it is not set explicitly, relaxed will be used.
///
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - value: instance of Encodable type `T` which will be encoded.
/// - Returns: Encoded representation of the `T` input as an instance of `Data` representing ExtendedJSON.
/// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON.
public func encode<T: Encodable>(_ value: T) throws -> Data {
private func encodeBytes<T: Encodable>(_ value: T) throws -> [UInt8] {
// T --> BSON --> JSONValue --> Data
// Takes in any encodable type `T`, converts it to an instance of the `BSON` enum via the `BSONDecoder`.
// The `BSON` is converted to an instance of the `JSON` enum via the `toRelaxedExtendedJSON`
Expand All @@ -53,6 +45,33 @@ public class ExtendedJSONEncoder {

var bytes: [UInt8] = []
json.value.appendBytes(to: &bytes)
return Data(bytes)
return bytes
}

/// Encodes an instance of the Encodable Type `T` into Data representing canonical or relaxed extended JSON.
/// The value of `self.mode` will determine which format is used. If it is not set explicitly, relaxed will be used.
///
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - value: instance of Encodable type `T` which will be encoded.
/// - Returns: Encoded representation of the `T` input as an instance of `Data` representing ExtendedJSON.
/// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON.
public func encode<T: Encodable>(_ value: T) throws -> Data {
try Data(self.encodeBytes(value))
}

/// Encodes an instance of the Encodable Type `T` into a `ByteBuffer` representing canonical or relaxed extended
/// JSON. The value of `self.mode` will determine which format is used. If it is not set explicitly, relaxed will
/// be used.
///
/// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
///
/// - Parameters:
/// - value: instance of Encodable type `T` which will be encoded.
/// - Returns: Encoded representation of the `T` input as an instance of `ByteBuffer` representing ExtendedJSON.
/// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON.
public func encodeBuffer<T: Encodable>(_ value: T) throws -> ByteBuffer {
try BSON_ALLOCATOR.buffer(bytes: self.encodeBytes(value))
}
}
7 changes: 7 additions & 0 deletions Tests/SwiftBSONTests/ExtendedJSONConversionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ open class ExtendedJSONConversionTestCase: BSONTestCase {
let regexStr = "{\"$regularExpression\":{\"pattern\":\"p\",\"options\":\"i\"}}"
let canonicalExtJSON = "{\"x\":true,\"y\":{\"$numberInt\":\"5\"},\"z\":\(regexStr)}"
let data = canonicalExtJSON.data(using: .utf8)!
let buffer = BSON_ALLOCATOR.buffer(bytes: data)
let regexObj = BSONRegularExpression(pattern: "p", options: "i")
let test = Test(x: true, y: 5, z: regexObj)

Expand All @@ -28,17 +29,23 @@ open class ExtendedJSONConversionTestCase: BSONTestCase {
encoder.mode = .canonical
let encoded: Data = try encoder.encode(test)
expect(encoded).to(cleanEqual(canonicalExtJSON))
let encodedBuffer = try encoder.encodeBuffer(test)
expect(Data(encodedBuffer.readableBytesView)).to(cleanEqual(canonicalExtJSON))

// Test relaxed encoder
encoder.mode = .relaxed
let relaxedEncoded: Data = try encoder.encode(test)
let relaxedExtJSON = "{\"x\":true,\"y\":5,\"z\":\(regexStr)}"
expect(relaxedEncoded).to(cleanEqual(relaxedExtJSON))
let relaxedEncodedBuffer = try encoder.encodeBuffer(test)
expect(Data(relaxedEncodedBuffer.readableBytesView)).to(cleanEqual(relaxedExtJSON))

// Test decoder
let decoder = ExtendedJSONDecoder()
let decoded = try decoder.decode(Test.self, from: data)
expect(decoded).to(equal(test))
let bufferDecoded = try decoder.decode(Test.self, from: buffer)
expect(bufferDecoded).to(equal(test))
}

func testExtendedJSONDecoderErrorKeyPath() throws {
Expand Down