Skip to content

Commit 973083d

Browse files
fix: optional url parameter encoding and add tests
1 parent 71686e8 commit 973083d

2 files changed

Lines changed: 173 additions & 33 deletions

File tree

Sources/Networking/Core/Requestable+Convenience.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,37 @@ private extension Requestable {
131131
return [URLQueryItem(name: key, value: parameter.encodedValue)]
132132

133133
default:
134-
return [
135-
URLQueryItem(name: key, value: String(describing: value))
136-
.percentEncoded()
137-
]
134+
if let stringValue = stringValue(value) {
135+
return [
136+
URLQueryItem(name: key, value: stringValue)
137+
.percentEncoded()
138+
]
139+
} else {
140+
return []
141+
}
138142
}
139143
}
140-
144+
145+
// Since `Any` type can be of type `Optional` under the hood, we want to make sure that the resulting string won't be encoded as "Optional({value})", because that's the default result of passing an `Optional` type to `String(describing: )` init.
146+
//
147+
// Example:
148+
// let optionalInt: Int? = 10
149+
// String(describing: optionalInt) == "Optional(10)"
150+
// stringValue(optionalInt) == "10"
151+
func stringValue(_ value: Any) -> String? {
152+
let mirror = Mirror(reflecting: value)
153+
154+
if mirror.displayStyle == .optional {
155+
if let first = mirror.children.first {
156+
return String(describing: first.value)
157+
} else {
158+
return nil
159+
}
160+
}
161+
162+
return String(describing: value)
163+
}
164+
141165
func buildArrayParameter(
142166
key: String,
143167
parameter: ArrayParameter

Tests/NetworkingTests/URLParametersTests.swift

Lines changed: 144 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,6 @@ import XCTest
1111
private let baseURLString = "https://requestable.tests"
1212

1313
final class URLParametersTests: XCTestCase {
14-
enum Router: Requestable {
15-
case urlParameters([String: any Sendable])
16-
17-
var baseURL: URL {
18-
// swiftlint:disable:next force_unwrapping
19-
URL(string: baseURLString)!
20-
}
21-
22-
var path: String {
23-
""
24-
}
25-
26-
var urlParameters: [String: Any]? {
27-
switch self {
28-
case let .urlParameters(parameters):
29-
parameters
30-
}
31-
}
32-
}
33-
3414
func testDefaultEncoding() async throws {
3515
let keyString = "name[first]"
3616
let keyPercentEncodedString = "name%5Bfirst%5D"
@@ -48,7 +28,7 @@ final class URLParametersTests: XCTestCase {
4828
let queryItems = percentEncodedQueryItems(from: url)
4929

5030
XCTAssertEqual(
51-
queryItems.first(where: { $0.name == keyPercentEncodedString })?.value,
31+
queryItems.value(for: keyPercentEncodedString),
5232
valuePercentEncodedString
5333
)
5434
}
@@ -65,7 +45,7 @@ final class URLParametersTests: XCTestCase {
6545

6646
let queryItems = percentEncodedQueryItems(from: url)
6747
XCTAssertEqual(
68-
queryItems.first(where: { $0.name == "date" })?.value,
48+
queryItems.value(for: "date"),
6949
dateString
7050
)
7151
}
@@ -83,7 +63,7 @@ final class URLParametersTests: XCTestCase {
8363

8464
let queryItems = percentEncodedQueryItems(from: url)
8565
XCTAssertEqual(
86-
queryItems.first(where: { $0.name == "date" })?.value,
66+
queryItems.value(for: "date"),
8767
datePlusSignPercentEncodedString
8868
)
8969
}
@@ -106,12 +86,12 @@ final class URLParametersTests: XCTestCase {
10686

10787
let queryItems = percentEncodedQueryItems(from: url)
10888
XCTAssertEqual(
109-
queryItems.first(where: { $0.name == "date" })?.value,
89+
queryItems.value(for: "date"),
11090
datePlusSignPercentEncodedString
11191
)
11292

11393
XCTAssertEqual(
114-
queryItems.first(where: { $0.name == "search" })?.value,
94+
queryItems.value(for: "search"),
11595
searchString
11696
)
11797
}
@@ -135,12 +115,12 @@ final class URLParametersTests: XCTestCase {
135115

136116
let queryItems = percentEncodedQueryItems(from: url)
137117
XCTAssertEqual(
138-
queryItems.first(where: { $0.name == "date" })?.value,
118+
queryItems.value(for: "date"),
139119
datePlusSignPercentEncodedString
140120
)
141121

142122
XCTAssertEqual(
143-
queryItems.first(where: { $0.name == "search" })?.value,
123+
queryItems.value(for: "search"),
144124
searchPercentEncodedString
145125
)
146126
}
@@ -159,16 +139,152 @@ final class URLParametersTests: XCTestCase {
159139

160140
let queryItems = percentEncodedQueryItems(from: url)
161141
XCTAssertEqual(
162-
queryItems.first(where: { $0.name == "date" })?.value,
142+
queryItems.value(for: "date"),
163143
customPercentEncodedString
164144
)
165145
}
146+
147+
func testOptionalsParametersEncodingWithValues() async throws {
148+
let parameters = OptionalParametersRouter.Parameters(
149+
int: 10,
150+
string: "testString",
151+
stringsArray: ["1", "2"]
152+
)
153+
let router = OptionalParametersRouter.test(parameters)
154+
155+
let request = try router.asRequest()
156+
157+
guard let url = request.url else {
158+
XCTFail("Can't create url from router")
159+
return
160+
}
161+
162+
let queryItems = percentEncodedQueryItems(from: url)
163+
164+
XCTAssertEqual(
165+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.int.stringValue),
166+
"10"
167+
)
168+
XCTAssertEqual(
169+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.string.stringValue),
170+
"testString"
171+
)
172+
XCTAssertEqual(
173+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.stringsArray.stringValue),
174+
"1,2"
175+
)
176+
}
177+
178+
func testOptionalsParametersEncodingWithNils() async throws {
179+
let parameters = OptionalParametersRouter.Parameters(
180+
int: 10,
181+
string: nil,
182+
stringsArray: nil
183+
)
184+
let router = OptionalParametersRouter.test(parameters)
185+
186+
let request = try router.asRequest()
187+
188+
guard let url = request.url else {
189+
XCTFail("Can't create url from router")
190+
return
191+
}
192+
193+
let queryItems = percentEncodedQueryItems(from: url)
194+
195+
XCTAssertEqual(
196+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.int.stringValue),
197+
"10"
198+
)
199+
XCTAssertEqual(
200+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.string.stringValue),
201+
nil
202+
)
203+
XCTAssertEqual(
204+
queryItems.value(for: OptionalParametersRouter.Parameters.CodingKeys.stringsArray.stringValue),
205+
nil
206+
)
207+
}
166208
}
167209

210+
// MARK: Helpers
168211
private extension URLParametersTests {
169212
// Helper method to create query items from URL to compare it with expected percent encoding
170213
func percentEncodedQueryItems(from: URL) -> [URLQueryItem] {
171214
let urlComponents = URLComponents(url: from, resolvingAgainstBaseURL: true)
172215
return urlComponents?.percentEncodedQueryItems ?? []
173216
}
174217
}
218+
219+
private extension [URLQueryItem] {
220+
func value(for key: String) -> String? {
221+
first(where: { $0.name == key })?.value
222+
}
223+
}
224+
225+
// MARK: Routers
226+
private enum Router: Requestable {
227+
case urlParameters([String: any Sendable])
228+
229+
var baseURL: URL {
230+
// swiftlint:disable:next force_unwrapping
231+
URL(string: baseURLString)!
232+
}
233+
234+
var path: String {
235+
""
236+
}
237+
238+
var urlParameters: [String: Any]? {
239+
switch self {
240+
case let .urlParameters(parameters):
241+
parameters
242+
.compactMapValues { $0 }
243+
}
244+
}
245+
}
246+
247+
private enum OptionalParametersRouter: Requestable {
248+
struct Parameters: Codable {
249+
enum CodingKeys: CodingKey {
250+
case int
251+
case string
252+
case stringsArray
253+
}
254+
255+
let int: Int?
256+
let string: String?
257+
let stringsArray: [String]?
258+
}
259+
260+
case test(Parameters)
261+
262+
var baseURL: URL {
263+
// swiftlint:disable:next force_unwrapping
264+
URL(string: baseURLString)!
265+
}
266+
267+
var path: String {
268+
""
269+
}
270+
271+
var urlParameters: [String: Any]? {
272+
switch self {
273+
case let .test(params):
274+
var urlParameters: [String: Any] = [
275+
Parameters.CodingKeys.int.stringValue: params.int as Any,
276+
Parameters.CodingKeys.string.stringValue: params.string as Any
277+
]
278+
279+
if let stringsArray = params.stringsArray {
280+
urlParameters[Parameters.CodingKeys.stringsArray.stringValue] = ArrayParameter(stringsArray, arrayEncoding: .commaSeparated)
281+
}
282+
283+
return urlParameters.compactMapValues { $0 }
284+
}
285+
}
286+
287+
var method: HTTPMethod {
288+
.get
289+
}
290+
}

0 commit comments

Comments
 (0)