diff --git a/Sources/MessagePack/Encoder/KeyedEncodingContainer.swift b/Sources/MessagePack/Encoder/KeyedEncodingContainer.swift index a657455..5cf5fbb 100644 --- a/Sources/MessagePack/Encoder/KeyedEncodingContainer.swift +++ b/Sources/MessagePack/Encoder/KeyedEncodingContainer.swift @@ -15,6 +15,10 @@ extension _MessagePackEncoder { self.codingPath = codingPath self.userInfo = userInfo } + + var sortKeys: Bool { + return userInfo[MessagePackEncoder.sortKeysKey] as? Bool ?? false + } } } @@ -77,7 +81,12 @@ extension _MessagePackEncoder.KeyedContainer: _MessagePackEncodingContainer { fatalError() } - for (key, container) in self.storage { + var storageToEncode = Array(self.storage) + if sortKeys { + storageToEncode.sort { $0.key.stringValue < $1.key.stringValue } + } + + for (key, container) in storageToEncode { let keyContainer = _MessagePackEncoder.SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo) try! keyContainer.encode(key.stringValue) data.append(keyContainer.data) diff --git a/Sources/MessagePack/Encoder/MessagePackEncoder.swift b/Sources/MessagePack/Encoder/MessagePackEncoder.swift index d2b6a08..2302a6c 100644 --- a/Sources/MessagePack/Encoder/MessagePackEncoder.swift +++ b/Sources/MessagePack/Encoder/MessagePackEncoder.swift @@ -23,6 +23,7 @@ final public class MessagePackEncoder { public func encode(_ value: T) throws -> Data where T : Encodable { let encoder = _MessagePackEncoder() encoder.userInfo = self.userInfo + encoder.userInfo[MessagePackEncoder.sortKeysKey] = sortKeys switch value { case let data as Data: @@ -35,6 +36,15 @@ final public class MessagePackEncoder { return encoder.data } + + /** + An option used by the encoder whether to sort keys while encoding keyed containers to achieve a deterministic output + */ + public var sortKeys: Bool = false + + internal static var sortKeysKey: CodingUserInfoKey { + return CodingUserInfoKey(rawValue: "sortKeysKey")! + } } // MARK: - TopLevelEncoder diff --git a/Tests/MessagePackTests/MessagePackEncodingTests.swift b/Tests/MessagePackTests/MessagePackEncodingTests.swift index 1e57684..f02ec00 100644 --- a/Tests/MessagePackTests/MessagePackEncodingTests.swift +++ b/Tests/MessagePackTests/MessagePackEncodingTests.swift @@ -6,6 +6,7 @@ class MessagePackEncodingTests: XCTestCase { override func setUp() { self.encoder = MessagePackEncoder() + self.encoder.sortKeys = true } func testEncodeNil() { @@ -59,16 +60,43 @@ class MessagePackEncodingTests: XCTestCase { } func testEncodeFixedDictionary() { - let value = try! encoder.encode(["a": 1]) - XCTAssertEqual(value, Data(bytes: [0x81, 0xA1, 0x61, 0x01])) + let value = try! encoder.encode(["a": 1, "b": 2]) + XCTAssertEqual(value, Data(bytes: [0x82, 0xA1, 0x61, 0x01, 0xA1, 0x62, 0x02])) } func testEncodeVariableDictionary() { let letters = "abcdefghijklmnopqrstuvwxyz".unicodeScalars let dictionary = Dictionary(uniqueKeysWithValues: zip(letters.map { String($0) }, 1...26)) let value = try! encoder.encode(dictionary) - XCTAssertEqual(value.count, 81) - XCTAssert(value.starts(with: [0xde] + [0x00, 0x1A])) + XCTAssertEqual(value, Data(bytes: [ + 0xDE, 0x0, 0x1A, + 0xA1, 0x61, 0x1, + 0xA1, 0x62, 0x2, + 0xA1, 0x63, 0x3, + 0xA1, 0x64, 0x4, + 0xA1, 0x65, 0x5, + 0xA1, 0x66, 0x6, + 0xA1, 0x67, 0x7, + 0xA1, 0x68, 0x8, + 0xA1, 0x69, 0x9, + 0xA1, 0x6A, 0xA, + 0xA1, 0x6B, 0xB, + 0xA1, 0x6C, 0xC, + 0xA1, 0x6D, 0xD, + 0xA1, 0x6E, 0xE, + 0xA1, 0x6F, 0xF, + 0xA1, 0x70, 0x10, + 0xA1, 0x71, 0x11, + 0xA1, 0x72, 0x12, + 0xA1, 0x73, 0x13, + 0xA1, 0x74, 0x14, + 0xA1, 0x75, 0x15, + 0xA1, 0x76, 0x16, + 0xA1, 0x77, 0x17, + 0xA1, 0x78, 0x18, + 0xA1, 0x79, 0x19, + 0xA1, 0x7A, 0x1A + ])) } func testEncodeData() {