Skip to content

Commit f54c4f7

Browse files
authored
Preserve Query Order (#57)
1 parent 6baa017 commit f54c4f7

File tree

9 files changed

+53
-27
lines changed

9 files changed

+53
-27
lines changed

Package.resolved

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ let package = Package(
1515
],
1616
dependencies: [
1717
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.5.0"),
18+
.package(url: "https://github.com/apple/swift-collections", from: "1.0.3"),
1819
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.10.0"),
1920
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.3.0"),
2021
.package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.1"),
@@ -23,6 +24,7 @@ let package = Package(
2324
.target(
2425
name: "URLRouting",
2526
dependencies: [
27+
.product(name: "OrderedCollections", package: "swift-collections"),
2628
.product(name: "Parsing", package: "swift-parsing"),
2729
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
2830
]

Sources/URLRouting/Cookies.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ extension Cookies: ParserPrinter where Parsers: ParserPrinter {
3838

3939
input.headers["cookie", default: []].prepend(
4040
cookies
41-
.sorted(by: { $0.key < $1.key })
4241
.flatMap { name, values in values.map { "\(name)=\($0 ?? "")" } }
4342
.joined(separator: "; ")[...]
4443
)

Sources/URLRouting/Field.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ extension Field: ParserPrinter where Value: ParserPrinter {
103103
@inlinable
104104
public func print(_ output: Value.Output, into input: inout URLRequestData.Fields) rethrows {
105105
if let defaultValue = self.defaultValue, isEqual(output, defaultValue) { return }
106-
input[self.name, default: []].prepend(try valueParser.print(output))
106+
try input.fields.updateValue(
107+
forKey: input.isNameCaseSensitive ? self.name : self.name.lowercased(),
108+
insertingDefault: [],
109+
at: 0,
110+
with: { $0.prepend(try self.valueParser.print(output)) }
111+
)
107112
}
108113
}

Sources/URLRouting/FormData.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ extension Data {
4646
init(encoding fields: URLRequestData.Fields) {
4747
self.init(
4848
fields
49-
.sorted(by: { $0.key < $1.key })
5049
.flatMap { pair -> [String] in
5150
let (name, values) = pair
5251
guard let name = name.addingPercentEncoding(withAllowedCharacters: .urlQueryParamAllowed)

Sources/URLRouting/Parsing/ParserPrinter.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ extension ParserPrinter where Input == URLRequestData {
6666
components.path = "/\(data.path.joined(separator: "/"))"
6767
if !data.query.isEmpty {
6868
components.queryItems = data.query
69-
.sorted(by: { $0.key < $1.key })
7069
.flatMap { name, values in
7170
values.map { URLQueryItem(name: name, value: $0.map(String.init)) }
7271
}

Sources/URLRouting/URLRequestData+Foundation.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ extension URLRequestData {
3636
query[item.name, default: []].append(item.value)
3737
} ?? [:],
3838
fragment: components.fragment,
39-
headers: request.allHTTPHeaderFields?.mapValues {
40-
$0.split(separator: ",", omittingEmptySubsequences: false).map { String($0) }
41-
} ?? [:],
39+
headers: .init(
40+
request.allHTTPHeaderFields?.map { key, value in
41+
(key, value.split(separator: ",", omittingEmptySubsequences: false).map { String($0) })
42+
} ?? [],
43+
uniquingKeysWith: { $1 }
44+
),
4245
body: request.httpBody
4346
)
4447
}
@@ -78,7 +81,6 @@ extension URLComponents {
7881
self.path = "/\(data.path.joined(separator: "/"))"
7982
if !data.query.isEmpty {
8083
self.queryItems = data.query
81-
.sorted(by: { $0.key < $1.key })
8284
.flatMap { name, values in
8385
values.map { URLQueryItem(name: name, value: $0.map(String.init)) }
8486
}
@@ -103,7 +105,7 @@ extension URLRequest {
103105
guard let url = URLComponents(data: data).url else { return nil }
104106
self.init(url: url)
105107
self.httpMethod = data.method
106-
for (name, values) in data.headers.sorted(by: { $0.key < $1.key }) {
108+
for (name, values) in data.headers {
107109
for value in values {
108110
if let value = value {
109111
self.addValue(String(value), forHTTPHeaderField: name)

Sources/URLRouting/URLRequestData.swift

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import OrderedCollections
23

34
/// A parseable URL request.
45
///
@@ -76,9 +77,9 @@ public struct URLRequestData: Equatable, _EmptyInitializable {
7677
host: String? = nil,
7778
port: Int? = nil,
7879
path: String = "",
79-
query: [String: [String?]] = [:],
80+
query: OrderedDictionary<String, [String?]> = [:],
8081
fragment: String? = nil,
81-
headers: [String: [String?]] = [:],
82+
headers: OrderedDictionary<String, [String?]> = [:],
8283
body: Data? = nil
8384
) {
8485
self.body = body
@@ -99,13 +100,13 @@ public struct URLRequestData: Equatable, _EmptyInitializable {
99100
/// Used by ``URLRequestData`` to model query parameters and headers in a way that can be
100101
/// efficiently parsed.
101102
public struct Fields {
102-
public var fields: [String: ArraySlice<Substring?>]
103+
public var fields: OrderedDictionary<String, ArraySlice<Substring?>>
103104

104105
@usableFromInline var isNameCaseSensitive: Bool
105106

106107
@inlinable
107108
public init(
108-
_ fields: [String: ArraySlice<Substring?>] = [:],
109+
_ fields: OrderedDictionary<String, ArraySlice<Substring?>> = [:],
109110
isNameCaseSensitive: Bool
110111
) {
111112
self.fields = [:]
@@ -152,9 +153,9 @@ extension URLRequestData: Codable {
152153
host: try container.decodeIfPresent(String.self, forKey: .host),
153154
port: try container.decodeIfPresent(Int.self, forKey: .port),
154155
path: try container.decodeIfPresent(String.self, forKey: .path) ?? "",
155-
query: try container.decodeIfPresent([String: [String?]].self, forKey: .query) ?? [:],
156+
query: try container.decodeIfPresent(OrderedDictionary<String, [String?]>.self, forKey: .query) ?? [:],
156157
fragment: try container.decodeIfPresent(String.self, forKey: .fragment),
157-
headers: try container.decodeIfPresent([String: [String?]].self, forKey: .headers) ?? [:],
158+
headers: try container.decodeIfPresent(OrderedDictionary<String, [String?]>.self, forKey: .headers) ?? [:],
158159
body: try container.decodeIfPresent(Data.self, forKey: .body)
159160
)
160161
}
@@ -219,27 +220,27 @@ extension URLRequestData: Hashable {
219220
}
220221

221222
extension URLRequestData.Fields: Collection {
222-
public typealias Element = Dictionary<String, ArraySlice<Substring?>>.Element
223-
public typealias Index = Dictionary<String, ArraySlice<Substring?>>.Index
223+
public typealias Element = OrderedDictionary<String, ArraySlice<Substring?>>.Element
224+
public typealias Index = OrderedDictionary<String, ArraySlice<Substring?>>.Index
224225

225226
@inlinable
226227
public var startIndex: Index {
227-
self.fields.startIndex
228+
self.fields.elements.startIndex
228229
}
229230

230231
@inlinable
231232
public var endIndex: Index {
232-
self.fields.endIndex
233+
self.fields.elements.endIndex
233234
}
234235

235236
@inlinable
236237
public subscript(position: Index) -> Element {
237-
self.fields[position]
238+
self.fields.elements[position]
238239
}
239240

240241
@inlinable
241242
public func index(after i: Index) -> Index {
242-
self.fields.index(after: i)
243+
self.fields.elements.index(after: i)
243244
}
244245
}
245246

@@ -253,11 +254,7 @@ extension URLRequestData.Fields: ExpressibleByDictionaryLiteral {
253254
extension URLRequestData.Fields: Equatable {
254255
@inlinable
255256
public static func == (lhs: Self, rhs: Self) -> Bool {
256-
guard lhs.count == rhs.count else { return false }
257-
for key in lhs.fields.keys {
258-
guard lhs[key] == rhs[key] else { return false }
259-
}
260-
return true
257+
lhs.fields == rhs.fields
261258
}
262259
}
263260

Tests/URLRoutingTests/URLRoutingTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class URLRoutingTests: XCTestCase {
7979
XCTAssertEqual("Blob", name)
8080
XCTAssertEqual(42, age)
8181
XCTAssertEqual(["debug": ["1"]], request.query)
82+
83+
XCTAssertEqual(
84+
try p.print(("Blob", 42)),
85+
URLRequestData(query: ["name": ["Blob"], "age": ["42"]])
86+
)
8287
}
8388

8489
func testQueryDefault() throws {
@@ -190,7 +195,7 @@ class URLRoutingTests: XCTestCase {
190195
try p.parse(&request)
191196
)
192197
XCTAssertEqual(
193-
URLRequestData(headers: ["cookie": ["isAdmin=true; userId=42"]]),
198+
URLRequestData(headers: ["cookie": ["userId=42; isAdmin=true"]]),
194199
try p.print(Session(userId: 42, isAdmin: true))
195200
)
196201
}

0 commit comments

Comments
 (0)