Request code WIP
This commit is contained in:
parent
1b81ee972e
commit
a3956d6870
@ -5,11 +5,13 @@ import PackageDescription
|
|||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "SwiftDevRant",
|
name: "SwiftDevRant",
|
||||||
|
platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6), .driverKit(.v19), .macCatalyst(.v13), .visionOS(.v1)],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
.library(
|
.library(
|
||||||
name: "SwiftDevRant",
|
name: "SwiftDevRant",
|
||||||
targets: ["SwiftDevRant"]),
|
targets: ["SwiftDevRant"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
|
19
Sources/SwiftDevRant/DevRantJSONCoder.swift
Normal file
19
Sources/SwiftDevRant/DevRantJSONCoder.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension JSONEncoder {
|
||||||
|
static let devRant: JSONEncoder = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.dateEncodingStrategy = .iso8601
|
||||||
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||||
|
return encoder
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JSONDecoder {
|
||||||
|
static let devRant: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601WithOptionalFractionalSeconds
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
return decoder
|
||||||
|
}()
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension JSONDecoder.DateDecodingStrategy {
|
||||||
|
nonisolated(unsafe) private static let dateFormatter: ISO8601DateFormatter = {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime]
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
nonisolated(unsafe) private static let dateFormatterWithFractionalSeconds: ISO8601DateFormatter = {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let iso8601WithOptionalFractionalSeconds: Self = {
|
||||||
|
return .custom { (decoder) -> Date in
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let dateString = try container.decode(String.self)
|
||||||
|
|
||||||
|
// Workaround to work with both, fractional seconds and whole seconds.
|
||||||
|
|
||||||
|
// Try whole seconds first:
|
||||||
|
|
||||||
|
if let date = dateFormatter.date(from: dateString) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it fails, try fractional:
|
||||||
|
|
||||||
|
if let date = dateFormatterWithFractionalSeconds.date(from: dateString) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Could not decode date from \(dateString).")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
3
Sources/SwiftDevRant/Request/Logger.swift
Normal file
3
Sources/SwiftDevRant/Request/Logger.swift
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
public protocol Logger {
|
||||||
|
func log(_ message: String)
|
||||||
|
}
|
@ -4,20 +4,22 @@ public struct Request {
|
|||||||
public enum Error<ApiError: Decodable & Sendable>: Swift.Error, CustomStringConvertible {
|
public enum Error<ApiError: Decodable & Sendable>: Swift.Error, CustomStringConvertible {
|
||||||
case notHttpResponse
|
case notHttpResponse
|
||||||
case notFound
|
case notFound
|
||||||
|
case noInternet
|
||||||
case apiError(_ error: ApiError)
|
case apiError(_ error: ApiError)
|
||||||
case generalError
|
case generalError
|
||||||
|
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .notHttpResponse: "response is not HTTP"
|
case .notHttpResponse: "Response is not HTTP"
|
||||||
case .notFound: "Not found"
|
case .notFound: "Not found"
|
||||||
|
case .noInternet: "No internet connection"
|
||||||
case .apiError(error: let error): "\(error)"
|
case .apiError(error: let error): "\(error)"
|
||||||
case .generalError: "General error"
|
case .generalError: "General error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmptyError: Decodable {
|
public struct EmptyError: Decodable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,9 +39,19 @@ public struct Request {
|
|||||||
var headers: [String: String] = [:]
|
var headers: [String: String] = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
var session = URLSession(configuration: .ephemeral)
|
let encoder: JSONEncoder
|
||||||
|
let decoder: JSONDecoder
|
||||||
|
let session: URLSession
|
||||||
|
let logger: Logger?
|
||||||
|
|
||||||
func makeURLRequest(config: Config, body: Data?) -> URLRequest {
|
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 {
|
||||||
let urlQuery = urlEncodedQueryString(from: config.urlParameters)
|
let urlQuery = urlEncodedQueryString(from: config.urlParameters)
|
||||||
guard let url = URL(string: config.backend.baseURL + config.path + urlQuery) else {
|
guard let url = URL(string: config.backend.baseURL + config.path + urlQuery) else {
|
||||||
fatalError("Couldn't create a URL")
|
fatalError("Couldn't create a URL")
|
||||||
@ -53,7 +65,7 @@ public struct Request {
|
|||||||
return urlRequest
|
return urlRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlEncodedQueryString(from query: [String: String]) -> String {
|
private func urlEncodedQueryString(from query: [String: String]) -> String {
|
||||||
guard !query.isEmpty else { return "" }
|
guard !query.isEmpty else { return "" }
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
|
components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
|
||||||
@ -61,4 +73,85 @@ public struct Request {
|
|||||||
let plusCorrection = absoluteString.replacingOccurrences(of: "+", with: "%2b")
|
let plusCorrection = absoluteString.replacingOccurrences(of: "+", with: "%2b")
|
||||||
return plusCorrection
|
return plusCorrection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult private func requestData<ApiError: Decodable>(urlRequest: URLRequest, apiError: ApiError.Type = EmptyError.self) async throws(Error<ApiError>) -> (data: Data, headers: [AnyHashable: Any]) {
|
||||||
|
do {
|
||||||
|
let response = try await session.data(for: urlRequest)
|
||||||
|
|
||||||
|
if let httpResponse = response.1 as? HTTPURLResponse {
|
||||||
|
let data = response.0
|
||||||
|
|
||||||
|
if let logger {
|
||||||
|
let logInputString = urlRequest.httpBody.flatMap { jsonString(data: $0, prettyPrinted: true) } ?? "(none)"
|
||||||
|
let logOutputString = !data.isEmpty ? 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
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if let error = error as? URLError, error.code == .notConnectedToInternet {
|
||||||
|
throw Error<ApiError>.noInternet
|
||||||
|
} else {
|
||||||
|
throw Error<ApiError>.generalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON Data to String converter for printing/logging purposes
|
||||||
|
private func jsonString(data: Data, prettyPrinted: Bool) -> String? {
|
||||||
|
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 {
|
||||||
|
print(error)
|
||||||
|
return String(data: data, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: public
|
||||||
|
|
||||||
|
public func requestJson<ApiError: Decodable>(config: Config, apiError: ApiError.Type = EmptyError.self) async throws(Error<ApiError>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
// The Swift Programming Language
|
public struct SwiftDevRant {
|
||||||
// https://docs.swift.org/swift-book
|
let request = Request(encoder: .devRant, decoder: .devRant)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user