Skip to content

Commit

Permalink
Preserve Query Order (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonkadelic authored Nov 7, 2022
1 parent 6baa017 commit f54c4f7
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 27 deletions.
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@
"version": "0.8.1"
}
},
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections",
"state": {
"branch": null,
"revision": "f504716c27d2e5d4144fa4794b12129301d17729",
"version": "1.0.3"
}
},
{
"package": "SwiftDocCPlugin",
"repositoryURL": "https://github.com/apple/swift-docc-plugin",
"state": {
"branch": null,
"revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
"version": "1.0.0"
}
},
{
"package": "swift-parsing",
"repositoryURL": "https://github.com/pointfreeco/swift-parsing",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.5.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.0.3"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.10.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.3.0"),
.package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.1"),
Expand All @@ -23,6 +24,7 @@ let package = Package(
.target(
name: "URLRouting",
dependencies: [
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "Parsing", package: "swift-parsing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
]
Expand Down
1 change: 0 additions & 1 deletion Sources/URLRouting/Cookies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ extension Cookies: ParserPrinter where Parsers: ParserPrinter {

input.headers["cookie", default: []].prepend(
cookies
.sorted(by: { $0.key < $1.key })
.flatMap { name, values in values.map { "\(name)=\($0 ?? "")" } }
.joined(separator: "; ")[...]
)
Expand Down
7 changes: 6 additions & 1 deletion Sources/URLRouting/Field.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ extension Field: ParserPrinter where Value: ParserPrinter {
@inlinable
public func print(_ output: Value.Output, into input: inout URLRequestData.Fields) rethrows {
if let defaultValue = self.defaultValue, isEqual(output, defaultValue) { return }
input[self.name, default: []].prepend(try valueParser.print(output))
try input.fields.updateValue(
forKey: input.isNameCaseSensitive ? self.name : self.name.lowercased(),
insertingDefault: [],
at: 0,
with: { $0.prepend(try self.valueParser.print(output)) }
)
}
}
1 change: 0 additions & 1 deletion Sources/URLRouting/FormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ extension Data {
init(encoding fields: URLRequestData.Fields) {
self.init(
fields
.sorted(by: { $0.key < $1.key })
.flatMap { pair -> [String] in
let (name, values) = pair
guard let name = name.addingPercentEncoding(withAllowedCharacters: .urlQueryParamAllowed)
Expand Down
1 change: 0 additions & 1 deletion Sources/URLRouting/Parsing/ParserPrinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ extension ParserPrinter where Input == URLRequestData {
components.path = "/\(data.path.joined(separator: "/"))"
if !data.query.isEmpty {
components.queryItems = data.query
.sorted(by: { $0.key < $1.key })
.flatMap { name, values in
values.map { URLQueryItem(name: name, value: $0.map(String.init)) }
}
Expand Down
12 changes: 7 additions & 5 deletions Sources/URLRouting/URLRequestData+Foundation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ extension URLRequestData {
query[item.name, default: []].append(item.value)
} ?? [:],
fragment: components.fragment,
headers: request.allHTTPHeaderFields?.mapValues {
$0.split(separator: ",", omittingEmptySubsequences: false).map { String($0) }
} ?? [:],
headers: .init(
request.allHTTPHeaderFields?.map { key, value in
(key, value.split(separator: ",", omittingEmptySubsequences: false).map { String($0) })
} ?? [],
uniquingKeysWith: { $1 }
),
body: request.httpBody
)
}
Expand Down Expand Up @@ -78,7 +81,6 @@ extension URLComponents {
self.path = "/\(data.path.joined(separator: "/"))"
if !data.query.isEmpty {
self.queryItems = data.query
.sorted(by: { $0.key < $1.key })
.flatMap { name, values in
values.map { URLQueryItem(name: name, value: $0.map(String.init)) }
}
Expand All @@ -103,7 +105,7 @@ extension URLRequest {
guard let url = URLComponents(data: data).url else { return nil }
self.init(url: url)
self.httpMethod = data.method
for (name, values) in data.headers.sorted(by: { $0.key < $1.key }) {
for (name, values) in data.headers {
for value in values {
if let value = value {
self.addValue(String(value), forHTTPHeaderField: name)
Expand Down
31 changes: 14 additions & 17 deletions Sources/URLRouting/URLRequestData.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import OrderedCollections

/// A parseable URL request.
///
Expand Down Expand Up @@ -76,9 +77,9 @@ public struct URLRequestData: Equatable, _EmptyInitializable {
host: String? = nil,
port: Int? = nil,
path: String = "",
query: [String: [String?]] = [:],
query: OrderedDictionary<String, [String?]> = [:],
fragment: String? = nil,
headers: [String: [String?]] = [:],
headers: OrderedDictionary<String, [String?]> = [:],
body: Data? = nil
) {
self.body = body
Expand All @@ -99,13 +100,13 @@ public struct URLRequestData: Equatable, _EmptyInitializable {
/// Used by ``URLRequestData`` to model query parameters and headers in a way that can be
/// efficiently parsed.
public struct Fields {
public var fields: [String: ArraySlice<Substring?>]
public var fields: OrderedDictionary<String, ArraySlice<Substring?>>

@usableFromInline var isNameCaseSensitive: Bool

@inlinable
public init(
_ fields: [String: ArraySlice<Substring?>] = [:],
_ fields: OrderedDictionary<String, ArraySlice<Substring?>> = [:],
isNameCaseSensitive: Bool
) {
self.fields = [:]
Expand Down Expand Up @@ -152,9 +153,9 @@ extension URLRequestData: Codable {
host: try container.decodeIfPresent(String.self, forKey: .host),
port: try container.decodeIfPresent(Int.self, forKey: .port),
path: try container.decodeIfPresent(String.self, forKey: .path) ?? "",
query: try container.decodeIfPresent([String: [String?]].self, forKey: .query) ?? [:],
query: try container.decodeIfPresent(OrderedDictionary<String, [String?]>.self, forKey: .query) ?? [:],
fragment: try container.decodeIfPresent(String.self, forKey: .fragment),
headers: try container.decodeIfPresent([String: [String?]].self, forKey: .headers) ?? [:],
headers: try container.decodeIfPresent(OrderedDictionary<String, [String?]>.self, forKey: .headers) ?? [:],
body: try container.decodeIfPresent(Data.self, forKey: .body)
)
}
Expand Down Expand Up @@ -219,27 +220,27 @@ extension URLRequestData: Hashable {
}

extension URLRequestData.Fields: Collection {
public typealias Element = Dictionary<String, ArraySlice<Substring?>>.Element
public typealias Index = Dictionary<String, ArraySlice<Substring?>>.Index
public typealias Element = OrderedDictionary<String, ArraySlice<Substring?>>.Element
public typealias Index = OrderedDictionary<String, ArraySlice<Substring?>>.Index

@inlinable
public var startIndex: Index {
self.fields.startIndex
self.fields.elements.startIndex
}

@inlinable
public var endIndex: Index {
self.fields.endIndex
self.fields.elements.endIndex
}

@inlinable
public subscript(position: Index) -> Element {
self.fields[position]
self.fields.elements[position]
}

@inlinable
public func index(after i: Index) -> Index {
self.fields.index(after: i)
self.fields.elements.index(after: i)
}
}

Expand All @@ -253,11 +254,7 @@ extension URLRequestData.Fields: ExpressibleByDictionaryLiteral {
extension URLRequestData.Fields: Equatable {
@inlinable
public static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.count == rhs.count else { return false }
for key in lhs.fields.keys {
guard lhs[key] == rhs[key] else { return false }
}
return true
lhs.fields == rhs.fields
}
}

Expand Down
7 changes: 6 additions & 1 deletion Tests/URLRoutingTests/URLRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class URLRoutingTests: XCTestCase {
XCTAssertEqual("Blob", name)
XCTAssertEqual(42, age)
XCTAssertEqual(["debug": ["1"]], request.query)

XCTAssertEqual(
try p.print(("Blob", 42)),
URLRequestData(query: ["name": ["Blob"], "age": ["42"]])
)
}

func testQueryDefault() throws {
Expand Down Expand Up @@ -190,7 +195,7 @@ class URLRoutingTests: XCTestCase {
try p.parse(&request)
)
XCTAssertEqual(
URLRequestData(headers: ["cookie": ["isAdmin=true; userId=42"]]),
URLRequestData(headers: ["cookie": ["userId=42; isAdmin=true"]]),
try p.print(Session(userId: 42, isAdmin: true))
)
}
Expand Down

0 comments on commit f54c4f7

Please sign in to comment.