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
4 changes: 0 additions & 4 deletions Sources/Certificate Pinning/Hasher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,4 @@ public class CKSHA256Hasher: Hasher {
extension Digest {
var bytes: [UInt8] { Array(makeIterator()) }
var data: Data { Data(bytes) }

var hexStr: String {
bytes.map { String(format: "%02X", $0) }.joined()
}
}
21 changes: 21 additions & 0 deletions Sources/ParameterEncoding/EncodingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// EncodingError.swift
// QuickHatch
//
// Created by Daniel Koster on 10/25/17.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public enum ParameterEncodingFailureReason : Sendable {
case missingURL
case jsonEncodingFailed(error: Error)
case propertyListEncodingFailed(error: Error)
}

public enum EncodingError: Error {
case stringDecodingFailed
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
case invalidURL(url: any URLConvertible)
}
41 changes: 41 additions & 0 deletions Sources/ParameterEncoding/EncodingHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// EncodingHelpers.swift
// QuickHatch
//
// Created by Daniel Koster on 8/14/19.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public struct EncodingHelpers {
public static func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []

if let dictionary = value as? [String: Any] {
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
} else if let array = value as? [Any] {
for value in array {
components += queryComponents(fromKey: "\(key)", value: value)
}
}
else if let bool = value as? Bool {
components.append((escape(key), escape((bool ? "1" : "0"))))
} else {
components.append((escape(key), escape("\(value)")))
}

return components
}

public static func escape(_ string: String) -> String {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="

var allowedCharacterSet = CharacterSet.urlQueryAllowed
allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
}
}
42 changes: 42 additions & 0 deletions Sources/ParameterEncoding/JSONEncoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// JSONEncoding.swift
// QuickHatch
//
// Created by Daniel Koster on 8/16/19.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public struct JSONEncoding: ParameterEncoding {

public static var `default`: JSONEncoding { return JSONEncoding() }

public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }

public let options: JSONSerialization.WritingOptions

public init(options: JSONSerialization.WritingOptions = []) {
self.options = options
}

public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()

guard let parameters = parameters else { return urlRequest }

do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)

if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
}

urlRequest.httpBody = data
} catch {
throw EncodingError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}

return urlRequest
}
}
46 changes: 46 additions & 0 deletions Sources/ParameterEncoding/ParameterEncoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// ParameterEncoding.swift
// QuickHatch
//
// Created by Daniel Koster on 10/25/17.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}

public typealias Parameters = [String: Any]

public protocol ParameterEncoding {
func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest
}

public enum ParamDestination {
case methodDependent
case queryString
case httpBody
}

public extension NSNumber {
var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
}

public extension Bool {
var stringValue: String {
return self ? "true" : "false"
}
var intValue: Int {
return self ? 1 : 0
}
}
26 changes: 26 additions & 0 deletions Sources/ParameterEncoding/ParameterTransformer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ParameterTransformer.swift
// QuickHatchHTTP
//
// Created by Daniel Koster on 8/29/25.
//
import Foundation

public protocol ParameterTransformer {
func transform(parameters: [String: any Sendable]) -> String
}

public struct DefaultParameterTransformer: ParameterTransformer {

public init() {

}

public func transform(parameters: [String : any Sendable]) -> String {
let parameters = parameters
.flatMap { (key, value) in EncodingHelpers.queryComponents(fromKey: key, value: value) }
.map { "\($0)=\($1)" }
.sorted(by: >)
return parameters.joined(separator: "&")
}
}
71 changes: 71 additions & 0 deletions Sources/ParameterEncoding/StringEncoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// StringEncoding.swift
// QuickHatch
//
// Created by Daniel Koster on 8/14/19.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public class StringEncoding: ParameterEncoding {

public init(destination: ParamDestination = .queryString) {
self.paramDest = destination
}
private var paramDest: ParamDestination

public class var bodyEncoding: StringEncoding {
return StringEncoding(destination: .httpBody)
}

public class var urlEncoding: StringEncoding {
return StringEncoding()
}

public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest {
var request: URLRequest = try urlRequest.asURLRequest()
guard let url = request.url else {
throw EncodingError.parameterEncodingFailed(reason: ParameterEncodingFailureReason.missingURL)
}
if paramDest == .queryString {
urlEncoding(url: url, params: parameters, urlRequest: &request)
return request
}
bodyEncoding(params: parameters, urlRequest: &request)
return request
}

private func urlEncoding(url: URL, params: Parameters?, urlRequest: inout URLRequest) {
guard let validParams = params, !validParams.isEmpty else { return }
var urlString = url.absoluteString
validParams.forEach({ (key, value) in
let escapedParameter = EncodingHelpers.escape("{\(key)}")
urlString = urlString.replacingOccurrences(of: escapedParameter, with: EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1)
})
urlRequest.url = URL(string: urlString)
}

private func bodyEncoding(params: Parameters?, urlRequest: inout URLRequest) {
guard let validParams = params, !validParams.isEmpty else { return }
var paramsEncoded: String = ""
validParams.forEach({ (key, value) in
paramsEncoded += EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1 + "&"
})
paramsEncoded.removeLast()
urlRequest.httpBody = paramsEncoded.data(using: .utf8, allowLossyConversion: false)
}

}

public struct StringURLTransformer: URLTransformer {
public func transform(url: String, parameters: [String : any Sendable]) -> String {
guard !parameters.isEmpty else { return url }
var urlString = url
for (key, value) in parameters {
let escapedKey = EncodingHelpers.escape("{\(key)}")
urlString = urlString.replacingOccurrences(of: escapedKey, with: EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1)
}
return urlString
}
}
80 changes: 80 additions & 0 deletions Sources/ParameterEncoding/URLEncoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// URLEncoding.swift
// QuickHatch
//
// Created by Daniel Koster on 8/16/19.
// Copyright © 2019 DaVinci Labs. All rights reserved.
//

import Foundation

public struct URLEncoding: ParameterEncoding {

public static var `default`: URLEncoding { return URLEncoding() }

public static var methodDependent: URLEncoding { return URLEncoding() }

public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }

public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }

public let destination: ParamDestination

public init(destination: ParamDestination = .methodDependent) {
self.destination = destination
}

public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()

guard let parameters = parameters else { return urlRequest }

if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodeParametersInURL(with: method) {
guard let url = urlRequest.url else {
throw EncodingError.parameterEncodingFailed(reason: .missingURL)
}

if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
urlComponents.percentEncodedQuery = percentEncodedQuery
urlRequest.url = urlComponents.url
}
} else {
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
}

urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
}

return urlRequest
}

private func query(_ parameters: [String: Any]) -> String {
var components: [(String, String)] = []

for key in parameters.keys.sorted(by: <) {
let value = parameters[key]!
components += EncodingHelpers.queryComponents(fromKey: key, value: value)
}
return components.map { "\($0)=\($1)" }.joined(separator: "&")
}

private func encodeParametersInURL(with method: HTTPMethod) -> Bool {
switch destination {
case .queryString:
return true
case .httpBody:
return false
default:
break
}

switch method {
case .get, .head, .delete:
return true
default:
return false
}
}
}
Loading
Loading