171 lines
7.6 KiB
Swift
Raw Normal View History

2024-12-10 11:05:03 +00:00
import Foundation
public struct Request {
public enum Error<ApiError: Decodable & Sendable>: Swift.Error, CustomStringConvertible {
case notHttpResponse
case notFound
2024-12-10 16:33:57 +00:00
case noInternet
2024-12-10 11:05:03 +00:00
case apiError(_ error: ApiError)
case generalError
public var description: String {
switch self {
2024-12-10 16:33:57 +00:00
case .notHttpResponse: "Response is not HTTP"
2024-12-10 11:05:03 +00:00
case .notFound: "Not found"
2024-12-10 16:33:57 +00:00
case .noInternet: "No internet connection"
2024-12-10 11:05:03 +00:00
case .apiError(error: let error): "\(error)"
case .generalError: "General error"
}
}
}
public struct EmptyError: Decodable, Swift.Error {
2024-12-10 11:05:03 +00:00
}
public enum Method: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
}
public struct Config {
let method: Method
let backend: Backend
let path: String
var urlParameters: [String: String] = [:]
var headers: [String: String] = [:]
}
2024-12-10 16:33:57 +00:00
let encoder: JSONEncoder
let decoder: JSONDecoder
let session: URLSession
let logger: Logger?
2024-12-10 11:05:03 +00:00
2024-12-10 16:33:57 +00:00
public init(encoder: JSONEncoder, decoder: JSONDecoder, session: URLSession = .init(configuration: .ephemeral), logger: Logger? = nil) {
self.encoder = encoder
self.decoder = decoder
self.session = session
self.logger = logger
}
private func makeURLRequest(config: Config, body: Data?) -> URLRequest {
2024-12-17 14:10:38 +00:00
let urlQuery = Self.urlEncodedQueryString(from: config.urlParameters)
2024-12-10 11:05:03 +00:00
guard let url = URL(string: config.backend.baseURL + config.path + urlQuery) else {
fatalError("Couldn't create a URL")
}
var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)
urlRequest.httpMethod = config.method.rawValue
urlRequest.httpBody = body
config.headers.forEach {
urlRequest.setValue($0.value, forHTTPHeaderField: $0.key)
}
return urlRequest
}
2024-12-17 14:10:38 +00:00
public static func urlEncodedQueryString(from query: [String: String]) -> String {
2024-12-10 11:05:03 +00:00
guard !query.isEmpty else { return "" }
var components = URLComponents()
components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
let absoluteString = components.url?.absoluteString ?? ""
let plusCorrection = absoluteString.replacingOccurrences(of: "+", with: "%2b")
return plusCorrection
}
2024-12-10 16:33:57 +00:00
2024-12-17 14:10:38 +00:00
@discardableResult private func requestData<ApiError: Decodable & Sendable>(urlRequest: URLRequest, apiError: ApiError.Type = EmptyError.self) async throws -> (data: Data, headers: [AnyHashable: Any]) {
let response: (Data, URLResponse)
2024-12-10 16:33:57 +00:00
do {
2024-12-17 14:10:38 +00:00
response = try await session.data(for: urlRequest)
2024-12-10 16:33:57 +00:00
} catch {
if let error = error as? URLError, error.code == .notConnectedToInternet {
throw Error<ApiError>.noInternet
} else {
throw Error<ApiError>.generalError
}
}
2024-12-17 14:10:38 +00:00
if let httpResponse = response.1 as? HTTPURLResponse {
let data = response.0
if let logger {
let logInputString = urlRequest.httpBody.flatMap { Self.jsonString(data: $0, prettyPrinted: true) } ?? "(none)"
let logOutputString = !data.isEmpty ? Self.jsonString(data: data, prettyPrinted: true) ?? "-" : "(none)"
logger.log("\(urlRequest.httpMethod?.uppercased() ?? "?") \(urlRequest.url?.absoluteString ?? "")\nbody: \(logInputString)\nresponse: \(logOutputString)")
}
if (200..<300).contains(httpResponse.statusCode) {
return (data, httpResponse.allHeaderFields)
} else if httpResponse.statusCode == 404 {
throw Error<ApiError>.notFound
} else {
throw Error<ApiError>.apiError(try decoder.decode(apiError, from: data))
}
} else {
throw Error<ApiError>.notHttpResponse
}
2024-12-10 16:33:57 +00:00
}
/// JSON Data to String converter for printing/logging purposes
2024-12-17 14:10:38 +00:00
public static func jsonString(data: Data, prettyPrinted: Bool) -> String? {
2024-12-10 16:33:57 +00:00
do {
let writingOptions: JSONSerialization.WritingOptions = prettyPrinted ? [.prettyPrinted] : []
let decoded: Data?
if String(data: data, encoding: .utf8) == "null" {
decoded = nil
} else if let string = String(data: data, encoding: .utf8), string.first == "\"", string.last == "\"" {
decoded = data
} else if let encodedDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
decoded = try JSONSerialization.data(withJSONObject: encodedDict, options: writingOptions)
} else if let encodedArray = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] {
decoded = try JSONSerialization.data(withJSONObject: encodedArray, options: writingOptions)
} else {
decoded = nil
}
return decoded.flatMap { String(data: $0, encoding: .utf8) }
} catch {
return String(data: data, encoding: .utf8)
}
}
// MARK: public
2024-12-17 14:10:38 +00:00
public func requestJson<ApiError: Decodable & Sendable>(config: Config, apiError: ApiError.Type = EmptyError.self) async throws {
2024-12-10 16:33:57 +00:00
let urlRequest = makeURLRequest(config: config, body: nil)
try await requestData(urlRequest: urlRequest, apiError: apiError)
}
public func requestJson<In: Encodable, ApiError: Decodable & Sendable>(config: Config, json: In, apiError: ApiError.Type = EmptyError.self) async throws {
let inData = try encoder.encode(json)
let urlRequest = makeURLRequest(config: config, body: inData)
try await requestData(urlRequest: urlRequest, apiError: apiError)
}
public func requestJson<Out: Decodable, ApiError: Decodable & Sendable>(config: Config, apiError: ApiError.Type = EmptyError.self) async throws -> Out {
let urlRequest = makeURLRequest(config: config, body: nil)
let outData = try await requestData(urlRequest: urlRequest, apiError: apiError).data
return try decoder.decode(Out.self, from: outData)
}
public func requestJson<In: Encodable, Out: Decodable, ApiError: Decodable & Sendable>(config: Config, json: In, apiError: ApiError.Type = EmptyError.self) async throws -> Out {
let inData = try encoder.encode(json)
let urlRequest = makeURLRequest(config: config, body: inData)
let outData = try await requestData(urlRequest: urlRequest, apiError: apiError).data
return try decoder.decode(Out.self, from: outData)
}
2024-12-17 14:10:38 +00:00
public func requestJson<Out: Decodable, ApiError: Decodable & Sendable>(config: Config, string: String, apiError: ApiError.Type = EmptyError.self) async throws -> Out {
let inData = string.data(using: .utf8)
let urlRequest = makeURLRequest(config: config, body: inData)
let outData = try await requestData(urlRequest: urlRequest, apiError: apiError).data
return try decoder.decode(Out.self, from: outData)
}
2024-12-20 15:19:17 +00:00
public func requestJson<Out: Decodable, ApiError: Decodable & Sendable>(config: Config, data: Data, apiError: ApiError.Type = EmptyError.self) async throws -> Out {
let urlRequest = makeURLRequest(config: config, body: data)
let outData = try await requestData(urlRequest: urlRequest, apiError: apiError).data
return try decoder.decode(Out.self, from: outData)
}
2024-12-10 11:05:03 +00:00
}