diff --git a/Sources/URLEncodedForm/Codable/URLEncodedFormEncoder.swift b/Sources/URLEncodedForm/Codable/URLEncodedFormEncoder.swift index 9d7eed4..0c3b10f 100644 --- a/Sources/URLEncodedForm/Codable/URLEncodedFormEncoder.swift +++ b/Sources/URLEncodedForm/Codable/URLEncodedFormEncoder.swift @@ -11,6 +11,23 @@ /// See [Mozilla's](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) docs for more information about /// url-encoded forms. public final class URLEncodedFormEncoder: DataEncoder { + /// The formatting of the output data. + public struct OutputFormatting : OptionSet { + /// The format's default value. + public let rawValue: UInt + + /// Creates an OutputFormatting value with the given raw value. + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + /// Produce output with dictionary keys sorted in lexicographic order. + public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) + } + + /// The output format to produce. Defaults to `[]`. + public var outputFormatting: OutputFormatting = [] + /// Create a new `URLEncodedFormEncoder`. public init() {} @@ -28,7 +45,7 @@ public final class URLEncodedFormEncoder: DataEncoder { let context = URLEncodedFormDataContext(.dict([:])) let encoder = _URLEncodedFormEncoder(context: context, codingPath: []) try encodable.encode(to: encoder) - let serializer = URLEncodedFormSerializer() + let serializer = URLEncodedFormSerializer(sortedKeys: self.outputFormatting.contains(.sortedKeys)) guard case .dict(let dict) = context.data else { throw URLEncodedFormError( identifier: "invalidTopLevel", diff --git a/Sources/URLEncodedForm/Data/URLEncodedFormSerializer.swift b/Sources/URLEncodedForm/Data/URLEncodedFormSerializer.swift index 3fc037e..0455dc8 100644 --- a/Sources/URLEncodedForm/Data/URLEncodedFormSerializer.swift +++ b/Sources/URLEncodedForm/Data/URLEncodedFormSerializer.swift @@ -3,19 +3,35 @@ import Bits /// Converts `[String: URLEncodedFormData]` structs to `Data`. final class URLEncodedFormSerializer { /// Default form url encoded serializer. - static let `default` = URLEncodedFormSerializer() + static let `default` = URLEncodedFormSerializer(sortedKeys: false) + + /// Produce JSON with dictionary keys sorted in lexicographic order. + private var sortedKeys: Bool /// Create a new form-urlencoded data serializer. - init() {} + init(sortedKeys: Bool) { + self.sortedKeys = sortedKeys + } /// Serializes the data. func serialize(_ URLEncodedFormEncoded: [String: URLEncodedFormData]) throws -> Data { var data: [Data] = [] - for (key, val) in URLEncodedFormEncoded { + var elements: [(key: String, value: URLEncodedFormData)]! + + if self.sortedKeys { + elements = URLEncodedFormEncoded.sorted { (left, right) -> Bool in + return left.key.compare(right.key, options: [.numeric, .caseInsensitive, .forcedOrdering]) == .orderedAscending + } + } else { + elements = URLEncodedFormEncoded.map { $0 } + } + + for (key, val) in elements { let key = try key.urlEncodedFormEncoded() let subdata = try serialize(val, forKey: key) data.append(subdata) } + return data.joinedWithAmpersands() } @@ -32,10 +48,21 @@ final class URLEncodedFormSerializer { /// Serializes a `[String: URLEncodedFormData]` at a given key. private func serialize(_ dictionary: [String: URLEncodedFormData], forKey key: Data) throws -> Data { - let values = try dictionary.map { subKey, value -> Data in + var elements: [(key: String, value: URLEncodedFormData)]! + + if self.sortedKeys { + elements = dictionary.sorted { (left, right) -> Bool in + return left.key.compare(right.key, options: [.numeric, .caseInsensitive, .forcedOrdering]) == .orderedAscending + } + } else { + elements = dictionary.map { $0 } + } + + let values = try elements.map { (subKey, value) -> Data in let keyPath = try [.leftSquareBracket] + subKey.urlEncodedFormEncoded() + [.rightSquareBracket] return try serialize(value, forKey: key + keyPath) } + return values.joinedWithAmpersands() } diff --git a/Tests/URLEncodedFormTests/URLEncodedFormCodableTests.swift b/Tests/URLEncodedFormTests/URLEncodedFormCodableTests.swift index 0c1a0da..9888d09 100644 --- a/Tests/URLEncodedFormTests/URLEncodedFormCodableTests.swift +++ b/Tests/URLEncodedFormTests/URLEncodedFormCodableTests.swift @@ -62,6 +62,15 @@ class URLEncodedFormCodableTests: XCTestCase { XCTAssertEqual(string?.contains("type=cat"), true) } + func testSortedDictionary() throws { + let user = User(name: "Tanner", age: 23, pets: ["Zizek", "Foo"], dict: ["a": 1, "b": 2]) + let encoder = URLEncodedFormEncoder() + encoder.outputFormatting = .sortedKeys + let data = try encoder.encode(user) + let string = String(data: data, encoding: .ascii) + XCTAssertEqual(string, "age=23&dict[a]=1&dict[b]=2&name=Tanner&pets[]=Zizek&pets[]=Foo") + } + /// https://github.com/vapor/url-encoded-form/issues/3 func testGH3() throws { struct Foo: Codable { @@ -77,6 +86,7 @@ class URLEncodedFormCodableTests: XCTestCase { ("testCodable", testCodable), ("testDecodeIntArray", testDecodeIntArray), ("testRawEnum", testRawEnum), + ("testSortedDictionary", testSortedDictionary), ("testGH3", testGH3), ] }