Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date decoding strategy #82

Merged
merged 4 commits into from
Apr 21, 2024
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
27 changes: 23 additions & 4 deletions Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct BSONDecoderSettings {
filterDollarPrefix: false,
stringDecodingStrategy: .string,
decodeObjectIdFromString: false,
timestampToDateDecodingStrategy: .never,
floatDecodingStrategy: .double,
doubleDecodingStrategy: .double,
int8DecodingStrategy: .anyInteger,
Expand All @@ -34,6 +35,7 @@ public struct BSONDecoderSettings {
filterDollarPrefix: false,
stringDecodingStrategy: .adaptive,
decodeObjectIdFromString: true,
timestampToDateDecodingStrategy: .relativeToReferenceDate,
floatDecodingStrategy: .adaptive,
doubleDecodingStrategy: .adaptive,
int8DecodingStrategy: .adaptive,
Expand Down Expand Up @@ -147,7 +149,17 @@ 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
}

/// If `true`, BSON Null values will be regarded as `nil`
public var decodeNullAsNil: Bool = true
public var filterDollarPrefix = false
Expand All @@ -158,9 +170,9 @@ 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
/// 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 +208,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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,22 @@ internal struct SingleValueBSONDecodingContainer: SingleValueDecodingContainer,

return date
} catch {
if decoder.settings.decodeDateFromTimestamp {
switch self.decoder.primitive {
let date: Date?
let strategy = decoder.settings.timestampToDateDecodingStrategy
switch self.decoder.primitive {
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.decoder.primitive) as! T
Expand Down
16 changes: 9 additions & 7 deletions Sources/BSON/Codable/Decoding/UnkeyedBSONDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,22 @@ internal struct UnkeyedBSONDecodingContainer: UnkeyedDecodingContainer {

return date
} catch {
if decoder.settings.decodeDateFromTimestamp {
switch element {
let date: Date?
let strategy = decoder.settings.timestampToDateDecodingStrategy
switch element {
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.nextElement()) as! T
Expand Down
36 changes: 17 additions & 19 deletions Tests/BSONTests/BSONCorpusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,23 +379,23 @@ final class BSONCorpusTests: XCTestCase {
// XCTAssertFalse(Document(bytes: [0x15,0x00,0x00,0x00,0x03,0x66,0x6F,0x6F,0x00,0x0A,0x00,0x00,0x00,0x08,0x62,0x61,0x72,0x00,0x01,0x00,0x00]).validate().valid)
// XCTAssertFalse(Document(bytes: [0x1C,0x00,0x00,0x00,0x03,0x66,0x6F,0x6F,0x00,0x12,0x00,0x00,0x00,0x02,0x62,0x61,0x72,0x00,0x05,0x00,0x00,0x00,0x62,0x61,0x7A,0x00,0x00,0x00]).validate().valid)
// }
//
// func testDateTime() {
// let doc0 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00])
// let doc1 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0xD8,0xD6,0xCC,0x3B,0x01,0x00,0x00,0x00])
// let doc2 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0x3C,0xE7,0xB9,0xBD,0xFF,0xFF,0xFF,0x00])
//
// XCTAssert(doc0.validate().valid)
// XCTAssert(doc1.validate().valid)
// XCTAssert(doc2.validate().valid)
//
// XCTAssertEqual(doc0["a"] as? Date, Date(timeIntervalSince1970: 0))
// XCTAssertEqual(doc1["a"] as? Date, Date(timeIntervalSince1970: 1356351330.500))
// XCTAssertEqual(doc2["a"] as? Date, Date(timeIntervalSince1970: -284643869.500))
//
// XCTAssertFalse(Document(bytes: [0x0C,0x00,0x00,0x00,0x09,0x61,0x00,0x12,0x34,0x56,0x78,0x00]).validate().valid)
// }
//

func testDateTime() {
let doc0 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00])
let doc1 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0xD8,0xD6,0xCC,0x3B,0x01,0x00,0x00,0x00])
let doc2 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0x3C,0xE7,0xB9,0xBD,0xFF,0xFF,0xFF,0x00])

XCTAssert(doc0.validate().isValid)
XCTAssert(doc1.validate().isValid)
XCTAssert(doc2.validate().isValid)

XCTAssertEqual(doc0["a"] as? Date, Date(timeIntervalSince1970: 0))
XCTAssertEqual(doc1["a"] as? Date, Date(timeIntervalSince1970: 1356351330.500))
XCTAssertEqual(doc2["a"] as? Date, Date(timeIntervalSince1970: -284643869.500))

XCTAssertFalse(Document(bytes: [0x0C,0x00,0x00,0x00,0x09,0x61,0x00,0x12,0x34,0x56,0x78,0x00]).validate().isValid)
}

// func testBinary() {
// let doc0 = Document(bytes: [0x0D,0x00,0x00,0x00,0x05,0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00])
// let doc1 = Document(bytes: [0x0F,0x00,0x00,0x00,0x05,0x78,0x00,0x02,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00])
Expand Down Expand Up @@ -525,5 +525,3 @@ final class BSONCorpusTests: XCTestCase {
//
// }
}


Loading