Skip to content

Commit 1eea7ba

Browse files
authored
Merge pull request #66 from strvcom/dev
Dev
2 parents eef4f84 + 7de2222 commit 1eea7ba

File tree

5 files changed

+298
-29
lines changed

5 files changed

+298
-29
lines changed

Sources/Networking/Core/Requestable+Convenience.swift

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,27 @@ public extension Requestable {
1515
var method: HTTPMethod {
1616
.get
1717
}
18-
18+
1919
/// By default the requestable API endpoint is unauthenticated.
2020
var isAuthenticationRequired: Bool {
2121
false
2222
}
23-
23+
2424
/// The default value is `nil`.
2525
var headers: [String: String]? {
2626
nil
2727
}
28-
28+
2929
/// The default value is `nil`.
3030
var urlParameters: [String: Any]? {
3131
nil
3232
}
33-
33+
3434
/// The default value is success & redirect http codes 200-399.
3535
var acceptableStatusCodes: Range<HTTPStatusCode>? {
3636
HTTPStatusCode.successAndRedirectCodes
3737
}
38-
38+
3939
/// The default value is `nil`.
4040
var dataType: RequestDataType? {
4141
nil
@@ -53,10 +53,10 @@ public extension Requestable {
5353
guard var urlComponents = URLComponents(url: urlPath, resolvingAgainstBaseURL: true) else {
5454
throw RequestableError.invalidURLComponents
5555
}
56-
56+
5757
// encode url parameters
5858
if let urlParameters {
59-
urlComponents.queryItems = buildQueryItems(urlParameters: urlParameters)
59+
urlComponents.percentEncodedQueryItems = buildPercentEncodedQueryItems(urlParameters: urlParameters)
6060
}
6161

6262
return urlComponents
@@ -74,12 +74,12 @@ public extension Requestable {
7474
return data
7575
}
7676
}
77-
77+
7878
func asRequest() throws -> URLRequest {
7979
guard let url = try urlComponents().url else {
8080
throw RequestableError.invalidURLComponents
8181
}
82-
82+
8383
// request setup
8484
var request = URLRequest(url: url)
8585
request.httpMethod = method.rawValue
@@ -101,43 +101,69 @@ public extension Requestable {
101101
default:
102102
break
103103
}
104-
104+
105105
return request
106106
}
107107
}
108108

109109
// MARK: Build Query Items
110110
private extension Requestable {
111-
func buildQueryItems(urlParameters: [String: Any]) -> [URLQueryItem] {
111+
func buildPercentEncodedQueryItems(urlParameters: [String: Any]) -> [URLQueryItem] {
112112
urlParameters
113113
.map { key, value -> [URLQueryItem] in
114-
buildQueryItems(key: key, value: value)
114+
buildPercentEncodedQueryItem(key: key, value: value)
115115
}
116116
.flatMap { $0 }
117117
}
118118

119-
func buildQueryItems(key: String, value: Any) -> [URLQueryItem] {
120-
if let arrayType = value as? ArrayParameter {
121-
var queryItems: [URLQueryItem] = []
119+
func buildPercentEncodedQueryItem(key: String, value: Any) -> [URLQueryItem] {
120+
switch value {
121+
case let parameter as ArrayParameter:
122+
return buildArrayParameter(
123+
key: key,
124+
parameter: parameter
125+
)
126+
127+
case let parameter as CustomEncodedParameter:
128+
return [URLQueryItem(name: key, value: parameter.encodedValue)]
122129

123-
switch arrayType.arrayEncoding {
124-
case .commaSeparated:
125-
queryItems = [URLQueryItem(
130+
default:
131+
return [
132+
URLQueryItem(name: key, value: String(describing: value))
133+
.percentEncoded()
134+
]
135+
}
136+
}
137+
138+
func buildArrayParameter(
139+
key: String,
140+
parameter: ArrayParameter
141+
) -> [URLQueryItem] {
142+
var queryItems: [URLQueryItem] = []
143+
144+
switch parameter.arrayEncoding {
145+
case .commaSeparated:
146+
queryItems = [
147+
URLQueryItem(
126148
name: key,
127-
value: arrayType.values.map { String(describing: $0) }.joined(separator: ",")
128-
)]
129-
130-
case .individual:
131-
for parameter in arrayType.values {
132-
queryItems.append(URLQueryItem(
149+
value: parameter.values
150+
.map { String(describing: $0) }
151+
.joined(separator: ",")
152+
)
153+
.percentEncoded()
154+
]
155+
156+
case .individual:
157+
for parameter in parameter.values {
158+
queryItems.append(
159+
URLQueryItem(
133160
name: key,
134161
value: String(describing: parameter)
135-
))
136-
}
162+
)
163+
.percentEncoded()
164+
)
137165
}
138-
return queryItems
139166
}
140-
141-
return [URLQueryItem(name: key, value: String(describing: value))]
167+
return queryItems
142168
}
143169
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// CustomEncodedParameter.swift
3+
//
4+
//
5+
// Created by Matej Molnár on 01.01.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// URL request query parameter that represents a value which will not be subjected to default percent encoding during URLRequest construction.
11+
///
12+
/// This type is useful in case you want to override the default percent encoding of some special characters with accordance to RFC3986.
13+
///
14+
/// Usage example:
15+
///
16+
/// var urlParameters: [String: Any]? {
17+
/// ["specialCharacter": ">"]
18+
/// }
19+
///
20+
/// // Request URL "https://test.com?specialCharacter=%3E"
21+
///
22+
/// var urlParameters: [String: Any]? {
23+
/// ["specialCharacter": PercentEncodedParameter(">")]
24+
/// }
25+
///
26+
/// // Request URL "https://test.com?specialCharacter=>"
27+
///
28+
29+
public struct CustomEncodedParameter {
30+
let encodedValue: String
31+
32+
public init(_ encodedValue: String) {
33+
self.encodedValue = encodedValue
34+
}
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// URLQueryItem+PercentEncoding.swift
3+
//
4+
//
5+
// Created by Tomas Cejka on 02.01.2024.
6+
//
7+
8+
import Foundation
9+
10+
/// Convenience methods to provide custom percent encoding for URLQueryItem
11+
extension URLQueryItem {
12+
13+
func percentEncoded() -> URLQueryItem {
14+
var newQueryItem = self
15+
newQueryItem.value = value?
16+
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
17+
18+
return newQueryItem
19+
}
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// String+PlusSignEncoded.swift
3+
//
4+
//
5+
// Created by Tomas Cejka on 17.02.2024.
6+
//
7+
8+
import Foundation
9+
10+
public extension String {
11+
/// Help method to allow custom + sign encoding, more in ```CustomEncodedParameter```
12+
func plusSignEncoded() -> Self? {
13+
self
14+
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)?
15+
.replacingOccurrences(of: "+", with: "%2B")
16+
}
17+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//
2+
// URLParametersTests.swift
3+
//
4+
//
5+
// Created by Matej Molnár on 02.01.2024.
6+
//
7+
8+
import Networking
9+
import XCTest
10+
11+
private let baseURLString = "https://requestable.tests"
12+
13+
final class URLParametersTests: XCTestCase {
14+
enum Router: Requestable {
15+
case urlParameters([String: Any])
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+
34+
func testDefaultEncoding() async throws {
35+
let nameString = "name]surname"
36+
let namePercentEncodedString = "name%5Dsurname"
37+
38+
let router = Router.urlParameters(["name": nameString])
39+
let request = try router.asRequest()
40+
41+
guard let url = request.url else {
42+
XCTFail("Can't create url from router")
43+
return
44+
}
45+
46+
let queryItems = percentEncodedQueryItems(from: url)
47+
XCTAssertEqual(
48+
queryItems.first(where: { $0.name == "name" })?.value,
49+
namePercentEncodedString
50+
)
51+
}
52+
53+
func testPlusSignDefaultEncoding() async throws {
54+
let dateString = "2023-11-29T12:13:04.598+0100"
55+
let router = Router.urlParameters(["date": dateString])
56+
let request = try router.asRequest()
57+
58+
guard let url = request.url else {
59+
XCTFail("Can't create url from router")
60+
return
61+
}
62+
63+
let queryItems = percentEncodedQueryItems(from: url)
64+
XCTAssertEqual(
65+
queryItems.first(where: { $0.name == "date" })?.value,
66+
dateString
67+
)
68+
}
69+
70+
func testPlusSignPercentEncodedParameter() async throws {
71+
let dateString = "2023-11-29T12:13:04.598+0100"
72+
let datePlusSignPercentEncodedString = "2023-11-29T12:13:04.598%2B0100"
73+
let router = Router.urlParameters(["date": CustomEncodedParameter(dateString.plusSignEncoded() ?? "")])
74+
let request = try router.asRequest()
75+
76+
guard let url = request.url else {
77+
XCTFail("Can't create url from router")
78+
return
79+
}
80+
81+
let queryItems = percentEncodedQueryItems(from: url)
82+
XCTAssertEqual(
83+
queryItems.first(where: { $0.name == "date" })?.value,
84+
datePlusSignPercentEncodedString
85+
)
86+
}
87+
88+
func testMixedPlusSignPercentEncodedParameter() async throws {
89+
let dateString = "2023-11-29T12:13:04.598+0100"
90+
let datePlusSignPercentEncodedString = "2023-11-29T12:13:04.598%2B0100"
91+
let searchString = "name+surname"
92+
93+
let router = Router.urlParameters([
94+
"date": CustomEncodedParameter(dateString.plusSignEncoded() ?? ""),
95+
"search": searchString
96+
])
97+
let request = try router.asRequest()
98+
99+
guard let url = request.url else {
100+
XCTFail("Can't create url from router")
101+
return
102+
}
103+
104+
let queryItems = percentEncodedQueryItems(from: url)
105+
XCTAssertEqual(
106+
queryItems.first(where: { $0.name == "date" })?.value,
107+
datePlusSignPercentEncodedString
108+
)
109+
110+
XCTAssertEqual(
111+
queryItems.first(where: { $0.name == "search" })?.value,
112+
searchString
113+
)
114+
}
115+
116+
func testMixedPercentEncodedParameter() async throws {
117+
let dateString = "2023-11-29T12:13:04.598+0100"
118+
let datePlusSignPercentEncodedString = "2023-11-29T12:13:04.598%2B0100"
119+
let searchString = "name+surnam]e"
120+
let searchPercentEncodedString = "name+surnam%5De"
121+
122+
let router = Router.urlParameters([
123+
"date": CustomEncodedParameter(dateString.plusSignEncoded() ?? ""),
124+
"search": searchString
125+
])
126+
let request = try router.asRequest()
127+
128+
guard let url = request.url else {
129+
XCTFail("Can't create url from router")
130+
return
131+
}
132+
133+
let queryItems = percentEncodedQueryItems(from: url)
134+
XCTAssertEqual(
135+
queryItems.first(where: { $0.name == "date" })?.value,
136+
datePlusSignPercentEncodedString
137+
)
138+
139+
XCTAssertEqual(
140+
queryItems.first(where: { $0.name == "search" })?.value,
141+
searchPercentEncodedString
142+
)
143+
}
144+
145+
func testCustomPercentEncodedParameter() async throws {
146+
let customPercentEncodedString = "2023-11-29T12:13:04.598%2B+%0100"
147+
let router = Router.urlParameters([
148+
"date": CustomEncodedParameter(customPercentEncodedString)
149+
])
150+
let request = try router.asRequest()
151+
152+
guard let url = request.url else {
153+
XCTFail("Can't create url from router")
154+
return
155+
}
156+
157+
let queryItems = percentEncodedQueryItems(from: url)
158+
XCTAssertEqual(
159+
queryItems.first(where: { $0.name == "date" })?.value,
160+
customPercentEncodedString
161+
)
162+
}
163+
}
164+
165+
private extension URLParametersTests {
166+
// Helper method to create query items from URL to compare it with expected percent encoding
167+
func percentEncodedQueryItems(from: URL) -> [URLQueryItem] {
168+
let urlComponents = URLComponents(url: from, resolvingAgainstBaseURL: true)
169+
return urlComponents?.percentEncodedQueryItems ?? []
170+
}
171+
}

0 commit comments

Comments
 (0)