Skip to content

Commit 04f787d

Browse files
committed
feat: implement query parameters formatter
1 parent f31458d commit 04f787d

File tree

8 files changed

+140
-11
lines changed

8 files changed

+140
-11
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// network-layer
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
import Foundation
7+
import NetworkLayerInterfaces
8+
9+
// MARK: - IQueryParametersFormatter
10+
11+
protocol IQueryParametersFormatter {
12+
func format(rawParameters: [AnyHashable: Any]) -> [String: String]
13+
}
14+
15+
// MARK: - QueryParametersFormatter
16+
17+
final class QueryParametersFormatter: IQueryParametersFormatter {
18+
// MARK: Properties
19+
20+
// Properties
21+
private let allowedCharacters: CharacterSet = {
22+
var restricted = CharacterSet(charactersIn: ":/?#[]@!$ &''()*+,;=\"<>%{}|\\^~`")
23+
restricted.formUnion(.newlines)
24+
return restricted.inverted
25+
}()
26+
27+
// MARK: Initialization
28+
29+
init() {}
30+
31+
// MARK: Internal
32+
33+
func format(rawParameters: [AnyHashable: Any]) -> [String: String] {
34+
var result: [String: String] = [:]
35+
rawParameters.forEach { key, value in
36+
guard
37+
let encodedKey = convertKeyToEncodedString(key),
38+
let encodedValue = convertValueToEncodedString(value)
39+
else {
40+
return
41+
}
42+
result[encodedKey] = encodedValue
43+
}
44+
return result
45+
}
46+
47+
// MARK: - Private
48+
49+
private func convertKeyToEncodedString(_ key: AnyHashable) -> String? {
50+
switch key {
51+
case let string as String:
52+
return encodeQueryComponent(string)
53+
case let encodedComponent as SpecificEncodedComponent:
54+
return encodedComponent.encodedValue
55+
case let convertible as CustomStringConvertible:
56+
return encodeQueryComponent(convertible.description)
57+
}
58+
}
59+
60+
private func convertValueToEncodedString(_ value: Any) -> String? {
61+
switch value {
62+
case let string as String:
63+
return encodeQueryComponent(string)
64+
case is [Any], is [String: Any]:
65+
guard
66+
let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted]),
67+
let jsonString = String(data: data, encoding: .utf8)
68+
else {
69+
return nil
70+
}
71+
return encodeQueryComponent(jsonString)
72+
case let encodedComponent as SpecificEncodedComponent:
73+
return encodedComponent.encodedValue
74+
case let convertible as CustomStringConvertible:
75+
return encodeQueryComponent(convertible.description)
76+
default:
77+
return encodeQueryComponent("\(value)")
78+
}
79+
}
80+
81+
private func encodeQueryComponent(_ component: String) -> String? {
82+
component.addingPercentEncoding(withAllowedCharacters: allowedCharacters)
83+
}
84+
}

Sources/NetworkLayer/Classes/Core/Builders/RequestBuilder/RequestBuilder.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// network-layer
3-
// Copyright © 2023 Space Code. All rights reserved.
3+
// Copyright © 2024 Space Code. All rights reserved.
44
//
55

66
import Foundation
@@ -11,15 +11,18 @@ final class RequestBuilder: IRequestBuilder, @unchecked Sendable {
1111

1212
private let parametersEncoder: IRequestParametersEncoder
1313
private let requestBodyEncoder: IRequestBodyEncoder
14+
private let queryFormatter: IQueryParametersFormatter
1415

1516
// MARK: Initialization
1617

1718
init(
1819
parametersEncoder: IRequestParametersEncoder,
19-
requestBodyEncoder: IRequestBodyEncoder
20+
requestBodyEncoder: IRequestBodyEncoder,
21+
queryFormatter: IQueryParametersFormatter
2022
) {
2123
self.parametersEncoder = parametersEncoder
2224
self.requestBodyEncoder = requestBodyEncoder
25+
self.queryFormatter = queryFormatter
2326
}
2427

2528
// MARK: IRequestBuilder
@@ -42,7 +45,8 @@ final class RequestBuilder: IRequestBuilder, @unchecked Sendable {
4245

4346
setHeaders(to: &urlRequest, headers: request.headers)
4447

45-
try parametersEncoder.encode(parameters: request.parameters ?? [:], to: &urlRequest)
48+
let parameters = queryFormatter.format(rawParameters: request.parameters ?? [:])
49+
try parametersEncoder.encode(parameters: parameters, to: &urlRequest)
4650

4751
if let httpBody = request.httpBody {
4852
try requestBodyEncoder.encode(body: httpBody, to: &urlRequest)

Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// network-layer
3-
// Copyright © 2023 Space Code. All rights reserved.
3+
// Copyright © 2024 Space Code. All rights reserved.
44
//
55

66
import Foundation
@@ -64,7 +64,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
6464
private var requestBuilder: IRequestBuilder {
6565
RequestBuilder(
6666
parametersEncoder: parametersEncoder,
67-
requestBodyEncoder: requestBodyEncoder
67+
requestBodyEncoder: requestBodyEncoder,
68+
queryFormatter: queryFormatter
6869
)
6970
}
7071

@@ -75,4 +76,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
7576
private var requestBodyEncoder: IRequestBodyEncoder {
7677
RequestBodyEncoder(jsonEncoder: jsonEncoder)
7778
}
79+
80+
private var queryFormatter: IQueryParametersFormatter {
81+
QueryParametersFormatter()
82+
}
7883
}

Sources/NetworkLayerInterfaces/Classes/Core/Models/IRequest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public protocol IRequest: Sendable {
1919
var headers: [String: String]? { get }
2020

2121
/// A dictionary that contains the parameters to be encoded into the request.
22-
var parameters: [String: String]? { get }
22+
var parameters: [AnyHashable: Any]? { get }
2323

2424
/// A Boolean value indicating whether authentication is required.
2525
var requiresAuthentication: Bool { get }
@@ -44,7 +44,7 @@ public extension IRequest {
4444
}
4545

4646
/// A dictionary that contains the parameters to be encoded into the request.
47-
var parameters: [String: String]? {
47+
var parameters: [AnyHashable: Any]? {
4848
nil
4949
}
5050

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//
2+
// network-layer
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
public protocol SpecificEncodedComponent {
7+
var encodedValue: String { get }
8+
}

Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// network-layer
3-
// Copyright © 2023 Space Code. All rights reserved.
3+
// Copyright © 2024 Space Code. All rights reserved.
44
//
55

66
import Foundation
@@ -23,7 +23,8 @@ extension RequestProcessor {
2323
),
2424
requestBuilder: RequestBuilder(
2525
parametersEncoder: RequestParametersEncoder(),
26-
requestBodyEncoder: RequestBodyEncoder(jsonEncoder: JSONEncoder())
26+
requestBodyEncoder: RequestBodyEncoder(jsonEncoder: JSONEncoder()),
27+
queryFormatter: QueryParametersFormatter()
2728
),
2829
dataRequestHandler: DataRequestHandler(),
2930
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, duration: .seconds(0))),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// network-layer
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
@testable import NetworkLayer
7+
8+
final class QueryParametersFormatterMock: IQueryParametersFormatter {
9+
var invokedFormat = false
10+
var invokedFormatCount = 0
11+
var invokedFormatParameters: ([AnyHashable: Any], Void)?
12+
var invokedFormatParametersList = [([AnyHashable: Any], Void)]()
13+
var stubbedFormat: [String: String]!
14+
func format(rawParameters: [AnyHashable: Any]) -> [String: String] {
15+
invokedFormat = true
16+
invokedFormatCount += 1
17+
invokedFormatParameters = (rawParameters, ())
18+
invokedFormatParametersList.append((rawParameters, ()))
19+
return stubbedFormat
20+
}
21+
}

Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestBuilderTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// network-layer
3-
// Copyright © 2023 Space Code. All rights reserved.
3+
// Copyright © 2024 Space Code. All rights reserved.
44
//
55

66
@testable import NetworkLayer
@@ -13,6 +13,7 @@ final class RequestBuilderTests: XCTestCase {
1313

1414
private var parametersEncoderMock: RequestParametersEncoderMock!
1515
private var requestBodyEncoderMock: RequestBodyEncoderMock!
16+
private var queryParametersFormatterMock: QueryParametersFormatterMock!
1617

1718
private var sut: RequestBuilder!
1819

@@ -22,16 +23,19 @@ final class RequestBuilderTests: XCTestCase {
2223
super.setUp()
2324
parametersEncoderMock = RequestParametersEncoderMock()
2425
requestBodyEncoderMock = RequestBodyEncoderMock()
26+
queryParametersFormatterMock = QueryParametersFormatterMock()
2527
sut = RequestBuilder(
2628
parametersEncoder: parametersEncoderMock,
27-
requestBodyEncoder: requestBodyEncoderMock
29+
requestBodyEncoder: requestBodyEncoderMock,
30+
queryFormatter: queryParametersFormatterMock
2831
)
2932
}
3033

3134
override func tearDown() {
3235
parametersEncoderMock = nil
3336
requestBodyEncoderMock = nil
3437
sut = nil
38+
queryParametersFormatterMock = nil
3539
super.tearDown()
3640
}
3741

@@ -62,6 +66,8 @@ final class RequestBuilderTests: XCTestCase {
6266
requestStub.stubbedHttpBody = .dictionary(.item)
6367
requestStub.stubbedParameters = .contentType
6468

69+
queryParametersFormatterMock.stubbedFormat = .contentType
70+
6571
// when
6672
var invokedConfigure = false
6773
let request = try sut.build(requestStub) { _ in invokedConfigure = true }

0 commit comments

Comments
 (0)