Skip to content

Commit aef9b5c

Browse files
authored
SWIFT-337 Support setting of encoding/decoding strategies on client, database, and collection levels (#208)
1 parent 1558b3b commit aef9b5c

15 files changed

+620
-114
lines changed

Sources/MongoSwift/BSON/BSONDecoder.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,25 @@ public class BSONDecoder {
109109
}
110110

111111
/// Initializes `self`.
112-
public init() {}
112+
public init(options: CodingStrategyProvider? = nil) {
113+
self.configureWithOptions(options: options)
114+
}
115+
116+
/// Initializes `self` by using the options of another `BSONDecoder` and the provided options, with preference
117+
/// going to the provided options in the case of conflicts.
118+
internal init(copies other: BSONDecoder, options: CodingStrategyProvider?) {
119+
self.userInfo = other.userInfo
120+
self.dateDecodingStrategy = other.dateDecodingStrategy
121+
self.uuidDecodingStrategy = other.uuidDecodingStrategy
122+
self.dataDecodingStrategy = other.dataDecodingStrategy
123+
self.configureWithOptions(options: options)
124+
}
125+
126+
internal func configureWithOptions(options: CodingStrategyProvider?) {
127+
self.dateDecodingStrategy = options?.dateCodingStrategy?.rawValue.decoding ?? self.dateDecodingStrategy
128+
self.uuidDecodingStrategy = options?.uuidCodingStrategy?.rawValue.decoding ?? self.uuidDecodingStrategy
129+
self.dataDecodingStrategy = options?.dataCodingStrategy?.rawValue.decoding ?? self.dataDecodingStrategy
130+
}
113131

114132
/**
115133
* Decodes a top-level value of the given type from the given BSON document.

Sources/MongoSwift/BSON/BSONEncoder.swift

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import mongoc
33

44
/// `BSONEncoder` facilitates the encoding of `Encodable` values into BSON.
55
public class BSONEncoder {
6-
/// Enum representing the various strategies for encoding `Date`s.
7-
///
8-
/// As per the BSON specification, the default strategy is to encode `Date`s as BSON datetime objects.
9-
///
10-
/// - SeeAlso: bsonspec.org
6+
/**
7+
* Enum representing the various strategies for encoding `Date`s.
8+
*
9+
* As per the BSON specification, the default strategy is to encode `Date`s as BSON datetime objects.
10+
*
11+
* - SeeAlso: bsonspec.org
12+
*/
1113
public enum DateEncodingStrategy {
1214
/// Encode the `Date` by deferring to its default encoding implementation.
1315
case deferredToDate
@@ -33,12 +35,14 @@ public class BSONEncoder {
3335
case custom((Date, Encoder) throws -> Void)
3436
}
3537

36-
/// Enum representing the various strategies for encoding `UUID`s.
37-
///
38-
/// As per the BSON specification, the default strategy is to encode `UUID`s as BSON binary types with the UUID
39-
/// subtype.
40-
///
41-
/// - SeeAlso: bsonspec.org
38+
/**
39+
* Enum representing the various strategies for encoding `UUID`s.
40+
*
41+
* As per the BSON specification, the default strategy is to encode `UUID`s as BSON binary types with the UUID
42+
* subtype.
43+
*
44+
* - SeeAlso: bsonspec.org
45+
*/
4246
public enum UUIDEncodingStrategy {
4347
/// Encode the `UUID` by deferring to its default encoding implementation.
4448
case deferredToUUID
@@ -47,18 +51,22 @@ public class BSONEncoder {
4751
case binary
4852
}
4953

50-
/// Enum representing the various strategies for encoding `Data`s.
51-
///
52-
/// As per the BSON specification, the default strategy is to encode `Data`s as BSON binary types with the generic
53-
/// binary subtype.
54-
///
55-
/// - SeeAlso: bsonspec.org
54+
/**
55+
* Enum representing the various strategies for encoding `Data`s.
56+
*
57+
* As per the BSON specification, the default strategy is to encode `Data`s as BSON binary types with the generic
58+
* binary subtype.
59+
*
60+
* - SeeAlso: bsonspec.org
61+
*/
5662
public enum DataEncodingStrategy {
57-
/// Encode the `Data` by deferring to its default encoding implementation.
58-
///
59-
/// Note: The default encoding implementation attempts to encode the `Data` as a `[UInt8]`, but because BSON
60-
/// does not support integer types besides `Int32` or `Int64`, it actually gets encoded to BSON as an `[Int32]`.
61-
/// This results in a space inefficient storage of the `Data` (using 4 bytes of BSON storage per byte of data).
63+
/**
64+
* Encode the `Data` by deferring to its default encoding implementation.
65+
*
66+
* Note: The default encoding implementation attempts to encode the `Data` as a `[UInt8]`, but because BSON
67+
* does not support integer types besides `Int32` or `Int64`, it actually gets encoded to BSON as an `[Int32]`.
68+
* This results in a space inefficient storage of the `Data` (using 4 bytes of BSON storage per byte of data).
69+
*/
6270
case deferredToData
6371

6472
/// Encode the `Data` as a BSON binary type (default).
@@ -102,7 +110,26 @@ public class BSONEncoder {
102110
}
103111

104112
/// Initializes `self`.
105-
public init() {}
113+
public init(options: CodingStrategyProvider? = nil) {
114+
self.configureWithOptions(options: options)
115+
}
116+
117+
/// Initializes `self` by using the options of another `BSONEncoder` and the provided options, with preference
118+
/// going to the provided options in the case of conflicts.
119+
internal init(copies other: BSONEncoder, options: CodingStrategyProvider?) {
120+
self.userInfo = other.userInfo
121+
self.dateEncodingStrategy = other.dateEncodingStrategy
122+
self.uuidEncodingStrategy = other.uuidEncodingStrategy
123+
self.dataEncodingStrategy = other.dataEncodingStrategy
124+
125+
self.configureWithOptions(options: options)
126+
}
127+
128+
internal func configureWithOptions(options: CodingStrategyProvider?) {
129+
self.dateEncodingStrategy = options?.dateCodingStrategy?.rawValue.encoding ?? self.dateEncodingStrategy
130+
self.uuidEncodingStrategy = options?.uuidCodingStrategy?.rawValue.encoding ?? self.uuidEncodingStrategy
131+
self.dataEncodingStrategy = options?.dataCodingStrategy?.rawValue.encoding ?? self.dataEncodingStrategy
132+
}
106133

107134
/**
108135
* Encodes the given top-level value and returns its BSON representation.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import Foundation
2+
3+
/// Protocol indicating a set of options can be used to configure `BSONEncoder` and `BSONDecoder`.
4+
public protocol CodingStrategyProvider {
5+
/// Specifies the strategy to use when converting `Date`s between their BSON representations and their
6+
/// representations in (non `Document`) `Codable` types.
7+
var dateCodingStrategy: DateCodingStrategy? { get }
8+
9+
/// Specifies the strategy to use when converting `UUID`s between their BSON representations and their
10+
/// representations in (non `Document`) `Codable` types.
11+
var uuidCodingStrategy: UUIDCodingStrategy? { get }
12+
13+
/// Specifies the strategy to use when converting `Data`s between their BSON representations and their
14+
/// representations in (non `Document`) `Codable` types.
15+
var dataCodingStrategy: DataCodingStrategy? { get }
16+
}
17+
18+
/// Options struct used for configuring the coding strategies on `BSONEncoder` and `BSONDecoder`.
19+
public struct BSONCoderOptions: CodingStrategyProvider {
20+
public var dateCodingStrategy: DateCodingStrategy?
21+
22+
public var uuidCodingStrategy: UUIDCodingStrategy?
23+
24+
public var dataCodingStrategy: DataCodingStrategy?
25+
26+
public init(dateCodingStrategy: DateCodingStrategy? = nil,
27+
uuidCodingStrategy: UUIDCodingStrategy? = nil,
28+
dataCodingStrategy: DataCodingStrategy? = nil) {
29+
self.dateCodingStrategy = dateCodingStrategy
30+
self.uuidCodingStrategy = uuidCodingStrategy
31+
self.dataCodingStrategy = dataCodingStrategy
32+
}
33+
}
34+
35+
/**
36+
* Enum representing the various encoding/decoding strategy pairs for `Date`s.
37+
* Set these on a `MongoClient`, `MongoDatabase`, or `MongoCollection` so that the strategies will be applied when
38+
* converting `Date`s between their BSON representations and their representations in (non `Document`) `Codable` types.
39+
*
40+
* As per the BSON specification, the default strategy is to encode `Date`s as BSON datetime objects.
41+
*
42+
* - SeeAlso: bsonspec.org
43+
*/
44+
public enum DateCodingStrategy: RawRepresentable {
45+
public typealias RawValue = (encoding: BSONEncoder.DateEncodingStrategy, decoding: BSONDecoder.DateDecodingStrategy)
46+
47+
/// Encode/decode the `Date` by deferring to its default encoding/decoding implementations.
48+
case deferredToDate
49+
50+
/// Encode/decode the `Date` to/from a BSON datetime object (default).
51+
case bsonDateTime
52+
53+
/// Encode/decode the `Date` to/from a 64-bit integer counting the number of milliseconds since January 1, 1970.
54+
case millisecondsSince1970
55+
56+
/// Encode/decode the `Date` to/from a BSON double counting the number of seconds since January 1, 1970.
57+
case secondsSince1970
58+
59+
/// Encode/decode the `Date` to/from an ISO-8601-formatted string (in RFC 339 format).
60+
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
61+
case iso8601
62+
63+
/// Encode/decode the `Date` to/from a string formatted by the given formatter.
64+
case formatted(DateFormatter)
65+
66+
/// Encode the `Date` by using the given `encodeFunc`. Decode the `Date` by using the given `decodeFunc`.
67+
/// If `encodeFunc` does not encode a value, an empty document will be encoded in its place.
68+
case custom(encodeFunc: (Date, Encoder) throws -> Void, decodeFunc: (Decoder) throws -> Date)
69+
70+
public init?(rawValue: RawValue) {
71+
switch rawValue {
72+
case (.deferredToDate, .deferredToDate):
73+
self = .deferredToDate
74+
case (.bsonDateTime, .bsonDateTime):
75+
self = .bsonDateTime
76+
case (.millisecondsSince1970, .millisecondsSince1970):
77+
self = .millisecondsSince1970
78+
case (.secondsSince1970, .secondsSince1970):
79+
self = .secondsSince1970
80+
case (.iso8601, .iso8601):
81+
guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
82+
fatalError("ISO8601DateFormatter is unavailable on this platform.")
83+
}
84+
self = .iso8601
85+
case let (.formatted(encodingFormatter), .formatted(decodingFormatter)):
86+
guard encodingFormatter == decodingFormatter else {
87+
return nil
88+
}
89+
self = .formatted(encodingFormatter)
90+
case let (.custom(encodeFunc), .custom(decodeFunc)):
91+
self = .custom(encodeFunc: encodeFunc, decodeFunc: decodeFunc)
92+
default:
93+
return nil
94+
}
95+
}
96+
97+
public var rawValue: RawValue {
98+
switch self {
99+
case .deferredToDate:
100+
return (.deferredToDate, .deferredToDate)
101+
case .bsonDateTime:
102+
return (.bsonDateTime, .bsonDateTime)
103+
case .millisecondsSince1970:
104+
return (.millisecondsSince1970, .millisecondsSince1970)
105+
case .secondsSince1970:
106+
return (.secondsSince1970, .secondsSince1970)
107+
case .iso8601:
108+
guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
109+
fatalError("ISO8601DateFormatter is unavailable on this platform.")
110+
}
111+
return (.iso8601, .iso8601)
112+
case let .formatted(formatter):
113+
return (.formatted(formatter), .formatted(formatter))
114+
case let .custom(encodeFunc, decodeFunc):
115+
return (.custom(encodeFunc), .custom(decodeFunc))
116+
}
117+
}
118+
}
119+
120+
/**
121+
* Enum representing the various encoding/decoding strategy pairs for `UUID`s.
122+
* Set these on a `MongoClient`, `MongoDatabase`, or `MongoCollection` so that the strategies will be applied when
123+
* converting `UUID`s between their BSON representations and their representations in (non `Document`) `Codable` types.
124+
*
125+
* As per the BSON specification, the default strategy is to encode `UUID`s as BSON binary types with the UUID
126+
* subtype.
127+
*
128+
* - SeeAlso: bsonspec.org
129+
*/
130+
public enum UUIDCodingStrategy: RawRepresentable {
131+
public typealias RawValue = (encoding: BSONEncoder.UUIDEncodingStrategy, decoding: BSONDecoder.UUIDDecodingStrategy)
132+
133+
/// Encode/decode the `UUID` by deferring to its default encoding/decoding implementations.
134+
case deferredToUUID
135+
136+
/// Encode/decode the `UUID` to/from a BSON binary type (default).
137+
case binary
138+
139+
public init?(rawValue: RawValue) {
140+
switch rawValue {
141+
case (.deferredToUUID, .deferredToUUID):
142+
self = .deferredToUUID
143+
case (.binary, .binary):
144+
self = .binary
145+
default:
146+
return nil
147+
}
148+
}
149+
150+
public var rawValue: RawValue {
151+
switch self {
152+
case .deferredToUUID:
153+
return (.deferredToUUID, .deferredToUUID)
154+
case .binary:
155+
return (.binary, .binary)
156+
}
157+
}
158+
}
159+
160+
/**
161+
* Enum representing the various encoding/decoding strategy pairs for `Data`s.
162+
* Set these on a `MongoClient`, `MongoDatabase`, or `MongoCollection` so that the strategies will be applied when
163+
* converting `Data`s between their BSON representations and their representations in (non `Document`) `Codable` types.
164+
*
165+
* As per the BSON specification, the default strategy is to encode `Data`s as BSON binary types with the generic
166+
* binary subtype.
167+
*
168+
* - SeeAlso: bsonspec.org
169+
*/
170+
public enum DataCodingStrategy: RawRepresentable {
171+
public typealias RawValue = (encoding: BSONEncoder.DataEncodingStrategy, decoding: BSONDecoder.DataDecodingStrategy)
172+
173+
/**
174+
* Encode/decode the `Data` by deferring to its default encoding implementations.
175+
*
176+
* Note: The default encoding implementation attempts to encode the `Data` as a `[UInt8]`, but because BSON
177+
* does not support integer types besides `Int32` or `Int64`, it actually gets encoded to BSON as an `[Int32]`.
178+
* This results in a space inefficient storage of the `Data` (using 4 bytes of BSON storage per byte of data).
179+
*/
180+
case deferredToData
181+
182+
/// Encode/decode the `Data` to/from a BSON binary type (default).
183+
case binary
184+
185+
/// Encode the `Data` to/from a base64 encoded string.
186+
case base64
187+
188+
/// Encode the `Data` by using the given `encodeFunc`. Decode the `Data` by using the given `decodeFunc`.
189+
/// If `encodeFunc` does not encode a value, an empty document will be encoded in its place.
190+
case custom(encodeFunc: (Data, Encoder) throws -> Void, decodeFunc: (Decoder) throws -> Data)
191+
192+
public init?(rawValue: RawValue) {
193+
switch rawValue {
194+
case (.deferredToData, .deferredToData):
195+
self = .deferredToData
196+
case (.binary, .binary):
197+
self = .binary
198+
case (.base64, .base64):
199+
self = .base64
200+
case let (.custom(encodeFunc), .custom(decodeFunc)):
201+
self = .custom(encodeFunc: encodeFunc, decodeFunc: decodeFunc)
202+
default:
203+
return nil
204+
}
205+
}
206+
207+
public var rawValue: RawValue {
208+
switch self {
209+
case .deferredToData:
210+
return (.deferredToData, .deferredToData)
211+
case .binary:
212+
return (.binary, .binary)
213+
case .base64:
214+
return (.base64, .base64)
215+
case let .custom(encodeFunc, decodeFunc):
216+
return (.custom(encodeFunc), .custom(decodeFunc))
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)