Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// network-layer
// Copyright © 2025 Space Code. All rights reserved.
//

import Foundation
import NetworkLayerInterfaces

// MARK: - IQueryParametersFormatter

protocol IQueryParametersFormatter {
func format(rawParameters: [AnyHashable: Any]) -> [String: String]
}

// MARK: - QueryParametersFormatter

final class QueryParametersFormatter: IQueryParametersFormatter {
// MARK: Properties

// Properties
private let allowedCharacters: CharacterSet = {
var restricted = CharacterSet(charactersIn: ":/?#[]@!$ &''()*+,;=\"<>%{}|\\^~`")
restricted.formUnion(.newlines)
return restricted.inverted
}()

// MARK: Initialization

init() {}

// MARK: Internal

func format(rawParameters: [AnyHashable: Any]) -> [String: String] {
var result: [String: String] = [:]
rawParameters.forEach { key, value in
guard
let encodedKey = convertKeyToEncodedString(key),
let encodedValue = convertValueToEncodedString(value)
else {
return
}
result[encodedKey] = encodedValue
}
return result
}

// MARK: - Private

private func convertKeyToEncodedString(_ key: AnyHashable) -> String? {
switch key {
case let string as String:
return encodeQueryComponent(string)
case let encodedComponent as SpecificEncodedComponent:
return encodedComponent.encodedValue
case let convertible as CustomStringConvertible:
return encodeQueryComponent(convertible.description)
}
}

private func convertValueToEncodedString(_ value: Any) -> String? {
switch value {
case let string as String:
return encodeQueryComponent(string)
case is [Any], is [String: Any]:
guard
let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .prettyPrinted]),
let jsonString = String(data: data, encoding: .utf8)
else {
return nil
}
return encodeQueryComponent(jsonString)
case let encodedComponent as SpecificEncodedComponent:
return encodedComponent.encodedValue
case let convertible as CustomStringConvertible:
return encodeQueryComponent(convertible.description)
default:
return encodeQueryComponent("\(value)")
}
}

private func encodeQueryComponent(_ component: String) -> String? {
component.addingPercentEncoding(withAllowedCharacters: allowedCharacters)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// network-layer
// Copyright © 2023 Space Code. All rights reserved.
// Copyright © 2024 Space Code. All rights reserved.
//

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

private let parametersEncoder: IRequestParametersEncoder
private let requestBodyEncoder: IRequestBodyEncoder
private let queryFormatter: IQueryParametersFormatter

// MARK: Initialization

init(
parametersEncoder: IRequestParametersEncoder,
requestBodyEncoder: IRequestBodyEncoder
requestBodyEncoder: IRequestBodyEncoder,
queryFormatter: IQueryParametersFormatter
) {
self.parametersEncoder = parametersEncoder
self.requestBodyEncoder = requestBodyEncoder
self.queryFormatter = queryFormatter
}

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

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

try parametersEncoder.encode(parameters: request.parameters ?? [:], to: &urlRequest)
let parameters = queryFormatter.format(rawParameters: request.parameters ?? [:])
try parametersEncoder.encode(parameters: parameters, to: &urlRequest)

if let httpBody = request.httpBody {
try requestBodyEncoder.encode(body: httpBody, to: &urlRequest)
Expand Down
9 changes: 7 additions & 2 deletions Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// network-layer
// Copyright © 2023 Space Code. All rights reserved.
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation
Expand Down Expand Up @@ -64,7 +64,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
private var requestBuilder: IRequestBuilder {
RequestBuilder(
parametersEncoder: parametersEncoder,
requestBodyEncoder: requestBodyEncoder
requestBodyEncoder: requestBodyEncoder,
queryFormatter: queryFormatter
)
}

Expand All @@ -75,4 +76,8 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
private var requestBodyEncoder: IRequestBodyEncoder {
RequestBodyEncoder(jsonEncoder: jsonEncoder)
}

private var queryFormatter: IQueryParametersFormatter {
QueryParametersFormatter()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public protocol IRequest: Sendable {
var headers: [String: String]? { get }

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

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

/// A dictionary that contains the parameters to be encoded into the request.
var parameters: [String: String]? {
var parameters: [AnyHashable: Any]? {
nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// network-layer
// Copyright © 2025 Space Code. All rights reserved.
//

public protocol SpecificEncodedComponent {
var encodedValue: String { get }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// network-layer
// Copyright © 2023 Space Code. All rights reserved.
// Copyright © 2024 Space Code. All rights reserved.
//

import Foundation
Expand All @@ -23,7 +23,8 @@ extension RequestProcessor {
),
requestBuilder: RequestBuilder(
parametersEncoder: RequestParametersEncoder(),
requestBodyEncoder: RequestBodyEncoder(jsonEncoder: JSONEncoder())
requestBodyEncoder: RequestBodyEncoder(jsonEncoder: JSONEncoder()),
queryFormatter: QueryParametersFormatter()
),
dataRequestHandler: DataRequestHandler(),
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, duration: .seconds(0))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// network-layer
// Copyright © 2025 Space Code. All rights reserved.
//

@testable import NetworkLayer

final class QueryParametersFormatterMock: IQueryParametersFormatter {
var invokedFormat = false
var invokedFormatCount = 0
var invokedFormatParameters: ([AnyHashable: Any], Void)?
var invokedFormatParametersList = [([AnyHashable: Any], Void)]()
var stubbedFormat: [String: String]!
func format(rawParameters: [AnyHashable: Any]) -> [String: String] {
invokedFormat = true
invokedFormatCount += 1
invokedFormatParameters = (rawParameters, ())
invokedFormatParametersList.append((rawParameters, ()))
return stubbedFormat
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// network-layer
// Copyright © 2023 Space Code. All rights reserved.
// Copyright © 2024 Space Code. All rights reserved.
//

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

private var parametersEncoderMock: RequestParametersEncoderMock!
private var requestBodyEncoderMock: RequestBodyEncoderMock!
private var queryParametersFormatterMock: QueryParametersFormatterMock!

private var sut: RequestBuilder!

Expand All @@ -22,16 +23,19 @@ final class RequestBuilderTests: XCTestCase {
super.setUp()
parametersEncoderMock = RequestParametersEncoderMock()
requestBodyEncoderMock = RequestBodyEncoderMock()
queryParametersFormatterMock = QueryParametersFormatterMock()
sut = RequestBuilder(
parametersEncoder: parametersEncoderMock,
requestBodyEncoder: requestBodyEncoderMock
requestBodyEncoder: requestBodyEncoderMock,
queryFormatter: queryParametersFormatterMock
)
}

override func tearDown() {
parametersEncoderMock = nil
requestBodyEncoderMock = nil
sut = nil
queryParametersFormatterMock = nil
super.tearDown()
}

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

queryParametersFormatterMock.stubbedFormat = .contentType

// when
var invokedConfigure = false
let request = try sut.build(requestStub) { _ in invokedConfigure = true }
Expand Down
Loading