From 504cd3938c55116f6acfbc6f0a00163f99e829de Mon Sep 17 00:00:00 2001 From: fumoboy007 Date: Thu, 16 Feb 2023 22:00:41 -0800 Subject: [PATCH] Support encoding/decoding `MessagePackTimestamp` using non-MessagePack encoders/decoders. `MessagePackTimestamp` currently requires a `MessagePackEncoder`/`MessagePackDecoder` for encoding/decoding. Using a non-MessagePack encoder/decoder results in a fatal error. A client may want to use a human-readable serialization format, such as JSON, in automated tests to make it easier to read and write the test. However, if a `MessagePackTimestamp` is present anywhere in the data structure to be serialized/deserialized, the test will crash. To enable the above use case, we add fallback behavior for non-MessagePack encoders/decoders when encoding/decoding `MessagePackTimestamp`. --- .../SingleValueDecodingContainer+.swift | 9 ++++++- .../SingleValueEncodingContainer+.swift | 9 ++++++- Sources/MessagePackTimestamp.swift | 25 +++++++++++++++++-- .../TimestampPackedTests.swift | 15 +++++++++++ .../TimestampUnpackedTests.swift | 14 +++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Sources/Extensions/SingleValueDecodingContainer+.swift b/Sources/Extensions/SingleValueDecodingContainer+.swift index 8566685..717bf6b 100644 --- a/Sources/Extensions/SingleValueDecodingContainer+.swift +++ b/Sources/Extensions/SingleValueDecodingContainer+.swift @@ -8,8 +8,15 @@ import Foundation +enum MessagePackableDecodingError: Error { + case notMessagePackDecoder +} + extension SingleValueDecodingContainer { func decode(as type: T.Type) throws -> T where T.T == T { - return try (self as! MessagePackDecoder.SingleValueContainer).decode(as: type) + guard let messagePackContainer = self as? MessagePackDecoder.SingleValueContainer else { + throw MessagePackableDecodingError.notMessagePackDecoder + } + return try messagePackContainer.decode(as: type) } } diff --git a/Sources/Extensions/SingleValueEncodingContainer+.swift b/Sources/Extensions/SingleValueEncodingContainer+.swift index 68d3574..b2120d5 100644 --- a/Sources/Extensions/SingleValueEncodingContainer+.swift +++ b/Sources/Extensions/SingleValueEncodingContainer+.swift @@ -8,8 +8,15 @@ import Foundation +enum MessagePackableEncodingError: Error { + case notMessagePackEncoder +} + extension SingleValueEncodingContainer { func encode(_ value: T) throws { - try (self as! MessagePackEncoder.SingleValueContainer).encode(from: value) + guard let messagePackContainer = self as? MessagePackEncoder.SingleValueContainer else { + throw MessagePackableEncodingError.notMessagePackEncoder + } + try messagePackContainer.encode(from: value) } } diff --git a/Sources/MessagePackTimestamp.swift b/Sources/MessagePackTimestamp.swift index 94f2e7b..559d2a0 100644 --- a/Sources/MessagePackTimestamp.swift +++ b/Sources/MessagePackTimestamp.swift @@ -47,12 +47,33 @@ extension MessagePackTimestamp { } extension MessagePackTimestamp: Codable { + enum CodingKeys: String, CodingKey { + case seconds + case nanoseconds + } + public func encode(to encoder: Encoder) throws { - try encoder.singleValueContainer().encode(self) + do { + // First, try encoding as a `MessagePackable`, which requires a `MessagePackEncoder`. + try encoder.singleValueContainer().encode(self) + } catch let error as MessagePackableEncodingError where error == .notMessagePackEncoder { + // If the caller is not using a `MessagePackEncoder`, then fall back to standard, non-MessagePack behavior. + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(seconds, forKey: .seconds) + try container.encode(nanoseconds, forKey: .nanoseconds) + } } public init(from decoder: Decoder) throws { - self = try decoder.singleValueContainer().decode(as: MessagePackTimestamp.self) + do { + // First, try decoding as a `MessagePackable`, which requires a `MessagePackDecoder`. + self = try decoder.singleValueContainer().decode(as: MessagePackTimestamp.self) + } catch let error as MessagePackableDecodingError where error == .notMessagePackDecoder { + // If the caller is not using a `MessagePackDecoder`, then fall back to standard, non-MessagePack behavior. + let container = try decoder.container(keyedBy: CodingKeys.self) + self.seconds = try container.decode(Int64.self, forKey: .seconds) + self.nanoseconds = try container.decode(Int64.self, forKey: .nanoseconds) + } } } diff --git a/Tests/MessagePackerTests/TimestampPackedTests.swift b/Tests/MessagePackerTests/TimestampPackedTests.swift index cd60305..bcc3d3a 100644 --- a/Tests/MessagePackerTests/TimestampPackedTests.swift +++ b/Tests/MessagePackerTests/TimestampPackedTests.swift @@ -37,4 +37,19 @@ class TimestampPackedTests: XCTestCase { let output = Data([199, 12, 255, 25, 69, 229, 222, 0, 0, 0, 14, 211, 132, 20, 74]) XCTAssertEqual(try encoder.encode(input), output) } + + func testSupportsNonMessagePackEncoder() { + let input = MessagePackTimestamp(seconds: 1542592042, nanoseconds: 209741115) + + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + let output = """ + { + "nanoseconds" : 209741115, + "seconds" : 1542592042 + } + """.data(using: .utf8)! + XCTAssertEqual(try jsonEncoder.encode(input), output) + } } diff --git a/Tests/MessagePackerTests/TimestampUnpackedTests.swift b/Tests/MessagePackerTests/TimestampUnpackedTests.swift index bac61c6..0122d84 100644 --- a/Tests/MessagePackerTests/TimestampUnpackedTests.swift +++ b/Tests/MessagePackerTests/TimestampUnpackedTests.swift @@ -69,4 +69,18 @@ class TimestampUnpackedTests: XCTestCase { XCTAssertEqual(try decoder.decode(Vehicle.self, from: input), output) } + + func testSupportsNonMessagePackDecoder() { + let input = """ + { + "nanoseconds" : 209741115, + "seconds" : 1542592042 + } + """.data(using: .utf8)! + + let jsonDecoder = JSONDecoder() + + let output = MessagePackTimestamp(seconds: 1542592042, nanoseconds: 209741115) + XCTAssertEqual(try jsonDecoder.decode(MessagePackTimestamp.self, from: input), output) + } }