Skip to content

Commit

Permalink
Merge pull request #83 from orlandos-nl/date-decoding-strategy
Browse files Browse the repository at this point in the history
Date decoding strategy, benchmarks and allow changing default BSON settings
  • Loading branch information
Joannis committed Apr 21, 2024
2 parents 7f9e743 + 6dbde5e commit 57da7f3
Show file tree
Hide file tree
Showing 10 changed files with 557 additions and 84 deletions.
77 changes: 77 additions & 0 deletions Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import BSON
import Benchmark

func bsonDecoderBenchmarks() {
let smallDocument: Document = [
"string": "Hello, world!",
"int": 42,
"double": 3.14159,
"bool": true,
"array": [1, 2, 3, 4, 5],
"document": ["key": "value"],
]

struct SmallType: Codable {
let string: String
let int: Int
let double: Double
let bool: Bool
let array: [Int]
let document: [String: String]
}

Benchmark("BSONDecoder:fastPath:small") { _ in
blackHole(
try BSONDecoder(settings: .fastPath)
.decode(SmallType.self, from: smallDocument)
)
}

Benchmark("BSONDecoder:adaptive:small") { _ in
blackHole(
try BSONDecoder(settings: .adaptive)
.decode(SmallType.self, from: smallDocument)
)
}

let largeDocument: Document = [
"string": "Hello, world!",
"int": 42,
"double": 3.14159,
"bool": true,
"array": [1, 2, 3, 4, 5],
"document": ["key": "value"],
"nested": [
"string": "Hello, world!",
"int": 42,
"double": 3.14159,
"bool": true,
"array": [1, 2, 3, 4, 5],
"document": ["key": "value"],
] as Document,
]

struct LargeType: Codable {
let string: String
let int: Int
let double: Double
let bool: Bool
let array: [Int]
let document: [String: String]
let nested: SmallType
}

Benchmark("BSONDecoder:fastPath:large") { _ in
blackHole(
try BSONDecoder(settings: .fastPath)
.decode(LargeType.self, from: largeDocument)
)
}

Benchmark("BSONDecoder:adaptive:large") { _ in
blackHole(
try BSONDecoder(settings: .adaptive)
.decode(LargeType.self, from: largeDocument)
)
}
}
13 changes: 13 additions & 0 deletions Benchmarks/Benchmarks/BSON/Benchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Benchmark

let benchmarks = {
Benchmark.defaultConfiguration = .init(
metrics: [
.cpuTotal,
.throughput,
.mallocCountTotal,
],
warmupIterations: 10
)
bsonDecoderBenchmarks()
}
27 changes: 27 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Benchmarks",
platforms: [.macOS(.v14)],
dependencies: [
.package(url: "https://github.com/ordo-one/package-benchmark.git", .upToNextMajor(from: "1.0.0")),
.package(path: "../"),
],
targets: [
// BSON benchmarks
.executableTarget(
name: "BSONBenchmarks",
dependencies: [
.product(name: "Benchmark", package: "package-benchmark"),
.product(name: "BSON", package: "BSON"),
],
path: "Benchmarks/BSON",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark"),
]
),
]
)
10 changes: 9 additions & 1 deletion Sources/BSON/Codable/Decoding/BSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct BSONDecoder {
public var userInfo: [CodingUserInfoKey: Any] = [:]

/// Creates a new decoder using fresh settings
public init(settings: BSONDecoderSettings = .adaptive) {
public init(settings: BSONDecoderSettings = .default) {
self.settings = settings
}
}
Expand Down Expand Up @@ -269,12 +269,20 @@ extension BSONDecoder {
if let value = primitive as? D {
return value
}

if self.settings.fastPath {
return try FastBSONDecoder().decode(D.self, from: primitive)
}

let decoder = _BSONDecoder(wrapped: .primitive(primitive), settings: self.settings, codingPath: [], userInfo: self.userInfo)
return try D(from: decoder)
}

public func decode<D: Decodable>(_ type: D.Type, from document: Document) throws -> D {
if self.settings.fastPath {
return try FastBSONDecoder().decode(D.self, from: document)
}

let decoder = _BSONDecoder(wrapped: .document(document), settings: self.settings, codingPath: [], userInfo: self.userInfo)
return try D(from: decoder)
}
Expand Down
146 changes: 103 additions & 43 deletions Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,72 @@ public struct BSONDecoderSettings {
///
/// - Float: Decode from Double
/// - Non-native Integer types: .anyInteger
public static var strict: BSONDecoderSettings {
return .init(
decodeNullAsNil: false,
filterDollarPrefix: false,
stringDecodingStrategy: .string,
decodeObjectIdFromString: false,
floatDecodingStrategy: .double,
doubleDecodingStrategy: .double,
int8DecodingStrategy: .anyInteger,
int16DecodingStrategy: .anyInteger,
int32DecodingStrategy: .int32,
int64DecodingStrategy: .int64,
intDecodingStrategy: .anyInteger,
uint8DecodingStrategy: .anyInteger,
uint16DecodingStrategy: .anyInteger,
uint32DecodingStrategy: .anyInteger,
uint64DecodingStrategy: .anyInteger,
uintDecodingStrategy: .anyInteger
)
}
public static let strict: BSONDecoderSettings = BSONDecoderSettings(
fastPath: false,
decodeNullAsNil: false,
filterDollarPrefix: false,
stringDecodingStrategy: .string,
decodeObjectIdFromString: false,
timestampToDateDecodingStrategy: .never,
floatDecodingStrategy: .double,
doubleDecodingStrategy: .double,
int8DecodingStrategy: .anyInteger,
int16DecodingStrategy: .anyInteger,
int32DecodingStrategy: .int32,
int64DecodingStrategy: .int64,
intDecodingStrategy: .anyInteger,
uint8DecodingStrategy: .anyInteger,
uint16DecodingStrategy: .anyInteger,
uint32DecodingStrategy: .anyInteger,
uint64DecodingStrategy: .anyInteger,
uintDecodingStrategy: .anyInteger
)

/// Uses ``FastBSONDecoder``
public static let fastPath: BSONDecoderSettings = BSONDecoderSettings(
fastPath: true,
decodeNullAsNil: false,
filterDollarPrefix: false,
stringDecodingStrategy: .string,
decodeObjectIdFromString: false,
timestampToDateDecodingStrategy: .never,
floatDecodingStrategy: .double,
doubleDecodingStrategy: .double,
int8DecodingStrategy: .anyInteger,
int16DecodingStrategy: .anyInteger,
int32DecodingStrategy: .int32,
int64DecodingStrategy: .int64,
intDecodingStrategy: .anyInteger,
uint8DecodingStrategy: .anyInteger,
uint16DecodingStrategy: .anyInteger,
uint32DecodingStrategy: .anyInteger,
uint64DecodingStrategy: .anyInteger,
uintDecodingStrategy: .anyInteger
)

public static var `default`: BSONDecoderSettings = .adaptive

/// Tries to decode values, even if the types do not match. Some precision loss is possible.
public static var adaptive: BSONDecoderSettings {
return .init(
decodeNullAsNil: true,
filterDollarPrefix: false,
stringDecodingStrategy: .adaptive,
decodeObjectIdFromString: true,
floatDecodingStrategy: .adaptive,
doubleDecodingStrategy: .adaptive,
int8DecodingStrategy: .adaptive,
int16DecodingStrategy: .adaptive,
int32DecodingStrategy: .adaptive,
int64DecodingStrategy: .adaptive,
intDecodingStrategy: .adaptive,
uint8DecodingStrategy: .adaptive,
uint16DecodingStrategy: .adaptive,
uint32DecodingStrategy: .adaptive,
uint64DecodingStrategy: .adaptive,
uintDecodingStrategy: .adaptive
)
}
public static let adaptive: BSONDecoderSettings = BSONDecoderSettings(
fastPath: false,
decodeNullAsNil: true,
filterDollarPrefix: false,
stringDecodingStrategy: .adaptive,
decodeObjectIdFromString: true,
timestampToDateDecodingStrategy: .relativeToUnixEpoch,
floatDecodingStrategy: .adaptive,
doubleDecodingStrategy: .adaptive,
int8DecodingStrategy: .adaptive,
int16DecodingStrategy: .adaptive,
int32DecodingStrategy: .adaptive,
int64DecodingStrategy: .adaptive,
intDecodingStrategy: .adaptive,
uint8DecodingStrategy: .adaptive,
uint16DecodingStrategy: .adaptive,
uint32DecodingStrategy: .adaptive,
uint64DecodingStrategy: .adaptive,
uintDecodingStrategy: .adaptive
)

/// A strategy used to decode `P` from a BSON `Primitive?` value
///
Expand Down Expand Up @@ -147,7 +171,19 @@ public struct BSONDecoderSettings {
/// This may be used for applying fallback values or other custom behaviour
case custom(DecodingStrategy<String>)
}


public enum TimestampToDateDecodingStrategy {

/// Do not convert, and throw an error
case never
/// Convert the timestamp relative to the unix epoch
case relativeToUnixEpoch
/// Convert the timestamp relative to the reference date, 1st of January 2000.
case relativeToReferenceDate
}

public var fastPath: Bool

/// If `true`, BSON Null values will be regarded as `nil`
public var decodeNullAsNil: Bool = true
public var filterDollarPrefix = false
Expand All @@ -157,10 +193,27 @@ public struct BSONDecoderSettings {

/// If `true`, allows decoding ObjectIds from Strings if they're formatted as a 24-character hexString
public var decodeObjectIdFromString: Bool = false

/// If `true`, allows decoding Date from a Double (TimeInterval)
public var decodeDateFromTimestamp: Bool = true
public var decodeDateFromTimestamp: Bool {
get { timestampToDateDecodingStrategy != .never }
set(decodeDateFromTimestamp) {
switch timestampToDateDecodingStrategy {
case .never:
if decodeDateFromTimestamp {
timestampToDateDecodingStrategy = .relativeToUnixEpoch
}
case .relativeToReferenceDate, .relativeToUnixEpoch:
if !decodeDateFromTimestamp {
timestampToDateDecodingStrategy = .never
}
}
}
}

/// A strategy to apply when converting time interval to date objects
public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToReferenceDate

/// A strategy that is applied when encountering a request to decode a `Float`
public var floatDecodingStrategy: FloatDecodingStrategy

Expand Down Expand Up @@ -196,4 +249,11 @@ public struct BSONDecoderSettings {

/// A strategy that is applied when encountering a request to decode a `UInt`
public var uintDecodingStrategy: IntegerDecodingStrategy<UInt>

public func with(timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy) -> Self {

var settings = self
settings.timestampToDateDecodingStrategy = timestampToDateDecodingStrategy
return settings
}
}
31 changes: 24 additions & 7 deletions Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,22 @@ internal struct KeyedBSONDecodingContainer<K: CodingKey>: KeyedDecodingContainer

return date
} catch {
if decoder.settings.decodeDateFromTimestamp {
switch self.document[key] {
let date: Date?
let strategy = decoder.settings.timestampToDateDecodingStrategy
switch self.document[key] {
case let int as Int:
return Date(timeIntervalSince1970: Double(int)) as! T
date = strategy.convertTimeStampToDate(TimeInterval(int))
case let int as Int32:
return Date(timeIntervalSince1970: Double(int)) as! T
date = strategy.convertTimeStampToDate(TimeInterval(int))
case let double as Double:
return Date(timeIntervalSince1970: double) as! T
date = strategy.convertTimeStampToDate(double)
default:
throw error
}
} else {
}
guard let returnDate = date as? T else {
throw error
}
return returnDate
}
} else if let type = T.self as? BSONPrimitiveConvertible.Type {
return try type.init(primitive: self.document[key]) as! T
Expand Down Expand Up @@ -238,3 +240,18 @@ internal struct KeyedBSONDecodingContainer<K: CodingKey>: KeyedDecodingContainer
return decoder
}
}

extension BSONDecoderSettings.TimestampToDateDecodingStrategy {

func convertTimeStampToDate(_ timestamp: TimeInterval) -> Date? {

switch self {
case .never:
return nil
case .relativeToUnixEpoch:
return Date(timeIntervalSince1970: timestamp)
case .relativeToReferenceDate:
return Date(timeIntervalSinceReferenceDate: timestamp)
}
}
}

0 comments on commit 57da7f3

Please sign in to comment.