Skip to content

Commit 029e800

Browse files
authored
Query Encoding Strategy (#18)
* Added a query encoding strategy * Slightly better semantics * Added one more test
1 parent 53512f0 commit 029e800

File tree

5 files changed

+82
-3
lines changed

5 files changed

+82
-3
lines changed

Sources/Endpoints/Endpoint+URLRequest.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ extension Endpoint {
7979
components.queryItems = urlQueryItems
8080
}
8181

82+
switch Self.queryEncodingStrategy {
83+
case .default:
84+
break
85+
case .custom(let encode):
86+
components.percentEncodedQuery = urlQueryItems
87+
.compactMap(encode)
88+
.compactMap { (name, value) in
89+
guard let value else { return nil }
90+
return name + "=" + value
91+
}
92+
.joined(separator: "&")
93+
}
94+
8295
let baseUrl = environment.baseUrl
8396
guard let url = components.url(relativeTo: baseUrl) else {
8497
throw EndpointError.invalid(components: components, relativeTo: baseUrl)

Sources/Endpoints/Endpoint.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ public protocol Endpoint {
139139

140140
/// The decoder instance to use when decoding the associated ``Endpoint/Response`` type
141141
static var responseDecoder: ResponseDecoder { get }
142+
143+
/// A strategy for encoding query parameters. Defaults to `QueryEncodingStrategy.default`
144+
static var queryEncodingStrategy: QueryEncodingStrategy { get }
142145
}
143146

144147
public extension Endpoint where Body == EmptyCodable {
@@ -174,6 +177,17 @@ public extension Endpoint where BodyEncoder == JSONEncoder {
174177
}
175178
}
176179

180+
public extension Endpoint {
181+
static var queryEncodingStrategy: QueryEncodingStrategy {
182+
return .default
183+
}
184+
}
185+
186+
public enum QueryEncodingStrategy {
187+
case `default`
188+
case custom((URLQueryItem) -> (String, String?)?)
189+
}
190+
177191
public struct Definition<T: Endpoint> {
178192

179193
/// The HTTP method of the ``Endpoint``
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// CustomEncodingEndpoint.swift
3+
// EndpointsTests
4+
//
5+
// Created by Zac White on 9/26/24.
6+
// Copyright © 2024 Velos Mobile LLC. All rights reserved.
7+
//
8+
9+
import Endpoints
10+
import Foundation
11+
12+
struct CustomEncodingEndpoint: Endpoint {
13+
static let definition: Definition<CustomEncodingEndpoint> = Definition(
14+
method: .get,
15+
path: "/",
16+
parameters: [
17+
.query("key", path: \ParameterComponents.needsCustomEncoding)
18+
]
19+
)
20+
21+
struct ParameterComponents {
22+
let needsCustomEncoding: String
23+
}
24+
25+
typealias Response = Void
26+
27+
let parameterComponents: ParameterComponents
28+
29+
static var queryEncodingStrategy: QueryEncodingStrategy {
30+
.custom {
31+
var characterSet = CharacterSet.urlQueryAllowed
32+
characterSet.remove(charactersIn: "+")
33+
return ($0.name, $0.value?.addingPercentEncoding(withAllowedCharacters: characterSet))
34+
}
35+
}
36+
}

Tests/EndpointsTests/Endpoints/UserEndpoint.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ struct UserEndpoint: Endpoint {
3636
]
3737
)
3838

39+
static var queryEncodingStrategy: QueryEncodingStrategy {
40+
.custom {
41+
var characterSet = CharacterSet.urlQueryAllowed
42+
characterSet.remove(charactersIn: "+")
43+
return ($0.name, $0.value?.addingPercentEncoding(withAllowedCharacters: characterSet))
44+
}
45+
}
46+
3947
typealias Response = Void
4048

4149
struct PathComponents {

Tests/EndpointsTests/EndpointsTests.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,20 @@ class EndpointsTests: XCTestCase {
6161
XCTAssertEqual(request.httpMethod, "POST")
6262
}
6363

64+
func testCustomParameterEncoding() throws {
65+
let request = try CustomEncodingEndpoint(
66+
parameterComponents: .init(needsCustomEncoding: "++++")
67+
).urlRequest(in: Environment.test)
68+
69+
XCTAssertEqual(request.url?.query, "key=%2B%2B%2B%2B")
70+
}
71+
6472
func testParameterEndpoint() throws {
6573

6674
let request = try UserEndpoint(
6775
pathComponents: .init(userId: "3"),
6876
parameterComponents: .init(
69-
string: "test:of:thing%asdf",
77+
string: "test:of:+thing%asdf",
7078
date: Date(),
7179
double: 2.3,
7280
int: 42,
@@ -81,7 +89,7 @@ class EndpointsTests: XCTestCase {
8189

8290
XCTAssertEqual(request.httpMethod, "GET")
8391
XCTAssertEqual(request.url?.path, "/hey/3")
84-
XCTAssertEqual(request.url?.query, "string=test:of:thing%25asdf&hard_coded_query=true")
92+
XCTAssertEqual(request.url?.query, "string=test:of:%2Bthing%25asdf&hard_coded_query=true")
8593

8694
XCTAssertEqual(request.value(forHTTPHeaderField: "HEADER_TYPE"), "test")
8795
XCTAssertEqual(request.value(forHTTPHeaderField: "HARD_CODED_HEADER"), "test2")
@@ -90,7 +98,7 @@ class EndpointsTests: XCTestCase {
9098

9199
XCTAssertNotNil(request.httpBody)
92100
XCTAssertTrue(
93-
String(data: request.httpBody ?? Data(), encoding: .utf8)?.contains("string=test%3Aof%3Athing%25asdf") ?? false
101+
String(data: request.httpBody ?? Data(), encoding: .utf8)?.contains("string=test%3Aof%3A+thing%25asdf") ?? false
94102
)
95103
XCTAssertFalse(
96104
String(data: request.httpBody ?? Data(), encoding: .utf8)?.contains("optional_string") ?? true

0 commit comments

Comments
 (0)