Skip to content

Commit aa44622

Browse files
authored
SWIFT-919 Extended JSON to BSON Conversion for all BSONValues
1 parent e0b0efb commit aa44622

20 files changed

+298
-49
lines changed

Sources/BSON/Array+BSONValue.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@ import NIO
22

33
/// An extension of `Array` to represent the BSON array type.
44
extension Array: BSONValue where Element == BSON {
5+
/*
6+
* Initializes an `Array` from ExtendedJSON.
7+
*
8+
* Parameters:
9+
* - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for an `Array`.
10+
* - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
11+
* This is used for error messages.
12+
*
13+
* Returns:
14+
* - `nil` if the provided value does not conform to the `Array` syntax.
15+
*
16+
* Throws:
17+
* - `DecodingError` if elements within the array is a partial match or is malformed.
18+
*/
19+
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
20+
// canonical and relaxed extended JSON
21+
guard case let .array(a) = json else {
22+
return nil
23+
}
24+
self = try a.enumerated().map { index, element in
25+
try BSON(fromExtJSON: element, keyPath: keyPath + [String(index)])
26+
}
27+
}
28+
529
internal static var bsonType: BSONType { .array }
630

731
internal var bson: BSON { .array(self) }

Sources/BSON/BSON.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ public enum BSON {
7777
}
7878
}
7979

80+
/// Initialize a `BSON` from ExtendedJSON
81+
/// Parameters:
82+
/// - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for any `BSONValue`.
83+
/// - `keyPath`: an array of `Strings`s containing the enclosing JSON keys of the current json being passed in.
84+
/// This is used for error messages.
85+
///
86+
/// Throws:
87+
/// - `DecodingError` if `json` is malformatted extended json
88+
internal init(fromExtJSON json: JSON, keyPath: [String]) throws {
89+
// Spec requires that we try int32, try int64, then try double for decoding numbers
90+
if let int32 = try Int32(fromExtJSON: json, keyPath: keyPath) {
91+
self = int32.bson
92+
return
93+
}
94+
if let int64 = try Int64(fromExtJSON: json, keyPath: keyPath) {
95+
self = int64.bson
96+
return
97+
}
98+
for bsonType in BSON.allBSONTypes.values {
99+
guard bsonType != Int32.self && bsonType != Int64.self && bsonType != BSONDocument.self else {
100+
continue
101+
}
102+
if let value = try bsonType.init(fromExtJSON: json, keyPath: keyPath) {
103+
self = value.bson
104+
return
105+
}
106+
}
107+
// Document accepts any JSON object so it should be tried after all the more specific BSON types are tried
108+
guard let doc = try BSONDocument(fromExtJSON: json, keyPath: keyPath) else {
109+
throw BSONError.InternalError(message: "Could not parse BSON from \(json)")
110+
}
111+
self = doc.bson
112+
}
113+
80114
/// Get the `BSONType` of this `BSON`.
81115
public var type: BSONType {
82116
self.bsonValue.bsonType

Sources/BSON/BSONBinary.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,15 @@ extension BSONBinary: BSONValue {
159159
*/
160160
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
161161
// canonical and relaxed extended JSON
162-
guard let (binary, obj) = try json.isObjectWithSingleKey(key: "$binary", keyPath: keyPath) else {
162+
guard let binary = try json.unwrapObject(withKey: "$binary", keyPath: keyPath) else {
163163
return nil
164164
}
165165
guard
166-
let binaryObj = binary.objectValue,
167-
binaryObj.count == 2,
168-
let base64 = binaryObj["base64"],
169-
let subTypeInput = binaryObj["subType"]
166+
let (base64, subTypeInput) = try binary.unwrapObject(withKeys: "base64", "subType", keyPath: keyPath)
170167
else {
171168
throw DecodingError._extendedJSONError(
172169
keyPath: keyPath,
173-
debugDescription: "Expected \"base64\" and \"subType\" keys in the object at \"$binary\", " +
174-
"found keys: \(obj.keys)"
170+
debugDescription: "Missing \"base64\" or \"subType\" in \(binary)"
175171
)
176172
}
177173
guard let base64Str = base64.stringValue else {

Sources/BSON/BSONCode.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension BSONCode: BSONValue {
4545
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
4646
switch json {
4747
case let .object(obj):
48-
// canonical extended JSON
48+
// canonical and relaxed extended JSON
4949
guard let value = obj["$code"] else {
5050
return nil
5151
}
@@ -88,6 +88,46 @@ extension BSONCode: BSONValue {
8888
}
8989

9090
extension BSONCodeWithScope: BSONValue {
91+
/*
92+
* Initializes a `BSONCode` from ExtendedJSON.
93+
*
94+
* Parameters:
95+
* - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for `Code`.
96+
* - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
97+
* This is used for error messages.
98+
*
99+
* Returns:
100+
* - `nil` if the provided value is not a `String`.
101+
*
102+
* Throws:
103+
* - `DecodingError` if `json` is a partial match or is malformed.
104+
*/
105+
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
106+
switch json {
107+
case let .object(obj):
108+
// canonical and relaxed extended JSON
109+
guard let (code, scope) = try json.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
110+
return nil
111+
}
112+
guard let codeStr = code.stringValue else {
113+
throw DecodingError._extendedJSONError(
114+
keyPath: keyPath,
115+
debugDescription: "Could not parse `BSONCodeWithScope` from \"code\": \"\(code)\"," +
116+
" input must be a string."
117+
)
118+
}
119+
guard let scopeDoc = try BSONDocument(fromExtJSON: scope, keyPath: keyPath + ["$scope"]) else {
120+
throw DecodingError._extendedJSONError(
121+
keyPath: keyPath,
122+
debugDescription: "Could not parse scope from \"\(scope)\", input must be a Document."
123+
)
124+
}
125+
self = BSONCodeWithScope(code: codeStr, scope: scopeDoc)
126+
default:
127+
return nil
128+
}
129+
}
130+
91131
internal static var bsonType: BSONType { .codeWithScope }
92132

93133
internal var bson: BSON { .codeWithScope(self) }

Sources/BSON/BSONDBPointer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension BSONDBPointer: BSONValue {
3232
*/
3333
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
3434
// canonical and relaxed extended JSON
35-
guard let (value, _) = try json.isObjectWithSingleKey(key: "$dbPointer", keyPath: keyPath) else {
35+
guard let value = try json.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
3636
return nil
3737
}
3838
guard let dbPointerObj = value.objectValue else {

Sources/BSON/BSONDecimal128.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ extension BSONDecimal128: BSONValue {
489489
*/
490490
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
491491
// canonical and relaxed extended JSON
492-
guard let (value, _) = try json.isObjectWithSingleKey(key: "$numberDecimal", keyPath: keyPath) else {
492+
guard let value = try json.unwrapObject(withKey: "$numberDecimal", keyPath: keyPath) else {
493493
return nil
494494
}
495495
guard let str = value.stringValue else {

Sources/BSON/BSONDocument.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,33 @@ extension BSONDocument: Equatable {
348348
}
349349

350350
extension BSONDocument: BSONValue {
351+
/*
352+
* Initializes a `BSONDocument` from ExtendedJSON.
353+
*
354+
* Parameters:
355+
* - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for any `BSONDocument`.
356+
* - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
357+
* This is used for error messages.
358+
*
359+
* Returns:
360+
* - `nil` if the provided value does not conform to the `BSONDocument` syntax.
361+
*
362+
* Throws:
363+
* - `DecodingError` if `json` is a partial match or is malformed.
364+
*/
365+
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
366+
// canonical and relaxed extended JSON
367+
guard case let .object(obj) = json else {
368+
return nil
369+
}
370+
var doc: [(String, BSON)] = []
371+
for (key, val) in obj {
372+
let bsonValue = try BSON(fromExtJSON: val, keyPath: keyPath + [key])
373+
doc.append((key, bsonValue))
374+
}
375+
self = BSONDocument(keyValuePairs: doc)
376+
}
377+
351378
internal static var bsonType: BSONType { .document }
352379

353380
internal var bson: BSON { .document(self) }

Sources/BSON/BSONEncoder.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,10 @@ private class MutableArray: BSONValue {
758758

759759
fileprivate init() {}
760760

761+
internal required init(fromExtJSON json: JSON, keyPath: [String]) throws {
762+
fatalError("MutableArray: BSONValue.init(fromExtJSON) should be unused")
763+
}
764+
761765
/// methods required by the BSONValue protocol that we don't actually need/use. MutableArray
762766
/// is just a BSONValue to simplify usage alongside true BSONValues within the encoder.
763767
static func read(from buffer: inout ByteBuffer) throws -> BSON {
@@ -842,6 +846,10 @@ private class MutableDictionary: BSONValue {
842846

843847
fileprivate init() {}
844848

849+
internal required init?(fromExtJSON json: JSON, keyPath: [String]) throws {
850+
fatalError("MutableDictionary: BSONValue.init(fromExtJSON) should be unused")
851+
}
852+
845853
/// methods required by the BSONValue protocol that we don't actually need/use. MutableDictionary
846854
/// is just a BSONValue to simplify usage alongside true BSONValues within the encoder.
847855
static func read(from buffer: inout ByteBuffer) throws -> BSON {

Sources/BSON/BSONNulls.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ internal struct BSONUndefined: BSONValue, Equatable {
5858
*/
5959
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
6060
// canonical and relaxed extended JSON
61-
guard let (value, obj) = try json.isObjectWithSingleKey(key: "$undefined", keyPath: keyPath) else {
61+
guard let value = try json.unwrapObject(withKey: "$undefined", keyPath: keyPath) else {
6262
return nil
6363
}
6464
guard value.boolValue == true else {
6565
throw DecodingError._extendedJSONError(
6666
keyPath: keyPath,
67-
debugDescription: "Expected \(obj) to be \"{\"$undefined\": true}\""
67+
debugDescription: "Expected \(value) to be \"true\""
6868
)
6969
}
7070
self = BSONUndefined()
@@ -104,13 +104,13 @@ internal struct BSONMinKey: BSONValue, Equatable {
104104
*/
105105
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
106106
// canonical and relaxed extended JSON
107-
guard let (value, obj) = try json.isObjectWithSingleKey(key: "$minKey", keyPath: keyPath) else {
107+
guard let value = try json.unwrapObject(withKey: "$minKey", keyPath: keyPath) else {
108108
return nil
109109
}
110110
guard value.doubleValue == 1 else {
111111
throw DecodingError._extendedJSONError(
112112
keyPath: keyPath,
113-
debugDescription: "Expected \(obj) to be \"{\"$minKey\": 1}\""
113+
debugDescription: "Expected \(value) to be \"1\""
114114
)
115115
}
116116
self = BSONMinKey()
@@ -150,13 +150,13 @@ internal struct BSONMaxKey: BSONValue, Equatable {
150150
*/
151151
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
152152
// canonical and relaxed extended JSON
153-
guard let (value, obj) = try json.isObjectWithSingleKey(key: "$maxKey", keyPath: keyPath) else {
153+
guard let value = try json.unwrapObject(withKey: "$maxKey", keyPath: keyPath) else {
154154
return nil
155155
}
156156
guard value.doubleValue == 1 else {
157157
throw DecodingError._extendedJSONError(
158158
keyPath: keyPath,
159-
debugDescription: "Expected \(obj) to be \"{\"$maxKey\": 1}\""
159+
debugDescription: "Expected \(value) to be \"1\""
160160
)
161161
}
162162
self = BSONMaxKey()

Sources/BSON/BSONObjectID.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public struct BSONObjectID: Equatable, Hashable, CustomStringConvertible {
7878
*/
7979
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
8080
// canonical and relaxed extended JSON
81-
guard let (value, _) = try json.isObjectWithSingleKey(key: "$oid", keyPath: keyPath) else {
81+
guard let value = try json.unwrapObject(withKey: "$oid", keyPath: keyPath) else {
8282
return nil
8383
}
8484
guard let str = value.stringValue else {

0 commit comments

Comments
 (0)