From d782a2fcf51ac5e64c97e7e2ad870bf4acbf3c25 Mon Sep 17 00:00:00 2001
From: Wilhelm Oks <oksw@MBP-von-Wilhelm.fritz.box>
Date: Fri, 20 Dec 2024 21:58:06 +0100
Subject: [PATCH] added KreeRequest as a dependency and adjusted the code to it

---
 Package.resolved                              |  15 ++
 Package.swift                                 |   6 +-
 .../ImageDataConversion.swift                 |   0
 .../{ => DevRant}/DevRantApiError.swift       |   1 +
 .../{ => DevRant}/DevRantBackend.swift        |   2 +
 .../{ => DevRant}/DevRantJSONCoder.swift      |   1 +
 Sources/SwiftDevRant/Request/Backend.swift    |   3 -
 ...ngStrategy+OptionalFractionalSeconds.swift |  38 ----
 Sources/SwiftDevRant/Request/Logger.swift     |   3 -
 Sources/SwiftDevRant/Request/Request.swift    | 175 ------------------
 Sources/SwiftDevRant/SwiftDevRant.swift       |  13 +-
 11 files changed, 31 insertions(+), 226 deletions(-)
 create mode 100644 Package.resolved
 rename Sources/SwiftDevRant/{ => Conversion}/ImageDataConversion.swift (100%)
 rename Sources/SwiftDevRant/{ => DevRant}/DevRantApiError.swift (83%)
 rename Sources/SwiftDevRant/{ => DevRant}/DevRantBackend.swift (80%)
 rename Sources/SwiftDevRant/{ => DevRant}/DevRantJSONCoder.swift (95%)
 delete mode 100644 Sources/SwiftDevRant/Request/Backend.swift
 delete mode 100644 Sources/SwiftDevRant/Request/JsonDecoder.DateDecodingStrategy+OptionalFractionalSeconds.swift
 delete mode 100644 Sources/SwiftDevRant/Request/Logger.swift
 delete mode 100644 Sources/SwiftDevRant/Request/Request.swift

diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..5e87354
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,15 @@
+{
+  "originHash" : "e6d5e17e67c8a09b38a3e356fa94b64f66f993b6102d12b3e4ad9adc4352c7d0",
+  "pins" : [
+    {
+      "identity" : "kreerequest",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/WilhelmOks/KreeRequest",
+      "state" : {
+        "revision" : "ce0d2ecdf923a9110b1439f22e868d22dd4ca006",
+        "version" : "1.0.2"
+      }
+    }
+  ],
+  "version" : 3
+}
diff --git a/Package.swift b/Package.swift
index 15a5be0..55ce723 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,11 +13,15 @@ let package = Package(
             targets: ["SwiftDevRant"]
         ),
     ],
+    dependencies: [
+        .package(url: "https://github.com/WilhelmOks/KreeRequest", .upToNextMajor(from: "1.0.2")),
+    ],
     targets: [
         // Targets are the basic building blocks of a package, defining a module or a test suite.
         // Targets can depend on other targets in this package and products from dependencies.
         .target(
-            name: "SwiftDevRant"
+            name: "SwiftDevRant",
+            dependencies: ["KreeRequest"]
         ),
         .testTarget(
             name: "SwiftDevRantTests",
diff --git a/Sources/SwiftDevRant/ImageDataConversion.swift b/Sources/SwiftDevRant/Conversion/ImageDataConversion.swift
similarity index 100%
rename from Sources/SwiftDevRant/ImageDataConversion.swift
rename to Sources/SwiftDevRant/Conversion/ImageDataConversion.swift
diff --git a/Sources/SwiftDevRant/DevRantApiError.swift b/Sources/SwiftDevRant/DevRant/DevRantApiError.swift
similarity index 83%
rename from Sources/SwiftDevRant/DevRantApiError.swift
rename to Sources/SwiftDevRant/DevRant/DevRantApiError.swift
index f3d624e..57e9921 100644
--- a/Sources/SwiftDevRant/DevRantApiError.swift
+++ b/Sources/SwiftDevRant/DevRant/DevRantApiError.swift
@@ -1,3 +1,4 @@
+/// Represents an error coming directly from the devrant API.
 public struct DevRantApiError: Swift.Error {
     let message: String
 }
diff --git a/Sources/SwiftDevRant/DevRantBackend.swift b/Sources/SwiftDevRant/DevRant/DevRantBackend.swift
similarity index 80%
rename from Sources/SwiftDevRant/DevRantBackend.swift
rename to Sources/SwiftDevRant/DevRant/DevRantBackend.swift
index 14bda95..bd0d034 100644
--- a/Sources/SwiftDevRant/DevRantBackend.swift
+++ b/Sources/SwiftDevRant/DevRant/DevRantBackend.swift
@@ -1,3 +1,5 @@
+import KreeRequest
+
 struct DevRantBackend: Backend {
     let baseURL = "https://devrant.com/api/"
 }
diff --git a/Sources/SwiftDevRant/DevRantJSONCoder.swift b/Sources/SwiftDevRant/DevRant/DevRantJSONCoder.swift
similarity index 95%
rename from Sources/SwiftDevRant/DevRantJSONCoder.swift
rename to Sources/SwiftDevRant/DevRant/DevRantJSONCoder.swift
index 8c104d3..8d23b80 100644
--- a/Sources/SwiftDevRant/DevRantJSONCoder.swift
+++ b/Sources/SwiftDevRant/DevRant/DevRantJSONCoder.swift
@@ -1,4 +1,5 @@
 import Foundation
+import KreeRequest
 
 extension JSONEncoder {
     static let devRant: JSONEncoder = {
diff --git a/Sources/SwiftDevRant/Request/Backend.swift b/Sources/SwiftDevRant/Request/Backend.swift
deleted file mode 100644
index a823620..0000000
--- a/Sources/SwiftDevRant/Request/Backend.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-public protocol Backend {
-    var baseURL: String { get }
-}
diff --git a/Sources/SwiftDevRant/Request/JsonDecoder.DateDecodingStrategy+OptionalFractionalSeconds.swift b/Sources/SwiftDevRant/Request/JsonDecoder.DateDecodingStrategy+OptionalFractionalSeconds.swift
deleted file mode 100644
index 3dd49b0..0000000
--- a/Sources/SwiftDevRant/Request/JsonDecoder.DateDecodingStrategy+OptionalFractionalSeconds.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-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).")
-        }
-    }()
-}
diff --git a/Sources/SwiftDevRant/Request/Logger.swift b/Sources/SwiftDevRant/Request/Logger.swift
deleted file mode 100644
index 7216f97..0000000
--- a/Sources/SwiftDevRant/Request/Logger.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-public protocol Logger {
-    func log(_ message: String)
-}
diff --git a/Sources/SwiftDevRant/Request/Request.swift b/Sources/SwiftDevRant/Request/Request.swift
deleted file mode 100644
index 91cf88f..0000000
--- a/Sources/SwiftDevRant/Request/Request.swift
+++ /dev/null
@@ -1,175 +0,0 @@
-import Foundation
-
-public struct Request {
-    public enum Error<ApiError: Decodable & Sendable>: Swift.Error, CustomStringConvertible {
-        case notHttpResponse
-        case notFound
-        case noInternet
-        case apiError(_ error: ApiError)
-        case generalError
-        
-        public var description: String {
-            switch self {
-            case .notHttpResponse: "Response is not HTTP"
-            case .notFound: "Not found"
-            case .noInternet: "No internet connection"
-            case .apiError(error: let error): "\(error)"
-            case .generalError: "General error"
-            }
-        }
-    }
-    
-    public struct EmptyError: Decodable, Swift.Error {
-        
-    }
-    
-    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] = [:]
-    }
-    
-    let encoder: JSONEncoder
-    let decoder: JSONDecoder
-    let session: URLSession
-    let logger: Logger?
-    
-    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 = Self.urlEncodedQueryString(from: config.urlParameters)
-        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
-    }
-    
-    public static func urlEncodedQueryString(from query: [String: String]) -> String {
-        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
-    }
-    
-    @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)
-        do {
-            response = try await session.data(for: urlRequest)
-        } catch {
-            if let error = error as? URLError, error.code == .notConnectedToInternet {
-                throw Error<ApiError>.noInternet
-            } else {
-                throw Error<ApiError>.generalError
-            }
-        }
-        
-        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
-        }
-    }
-    
-    /// JSON Data to String converter for printing/logging purposes
-    public static 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 {
-            return String(data: data, encoding: .utf8)
-        }
-    }
-    
-    // MARK: public
-    
-    public func requestJson<ApiError: Decodable & Sendable>(config: Config, apiError: ApiError.Type = EmptyError.self) async throws {
-        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)
-    }
-    
-    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)
-    }
-    
-    public func requestJson<ApiError: Decodable & Sendable>(config: Config, data: Data, apiError: ApiError.Type = EmptyError.self) async throws {
-        let urlRequest = makeURLRequest(config: config, body: data)
-        try await requestData(urlRequest: urlRequest, apiError: apiError).data
-    }
-    
-    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)
-    }
-}
diff --git a/Sources/SwiftDevRant/SwiftDevRant.swift b/Sources/SwiftDevRant/SwiftDevRant.swift
index f484cd5..3268838 100644
--- a/Sources/SwiftDevRant/SwiftDevRant.swift
+++ b/Sources/SwiftDevRant/SwiftDevRant.swift
@@ -1,14 +1,15 @@
 import Foundation
+import KreeRequest
 
 public struct SwiftDevRant {
-    let request: Request
+    let request: KreeRequest
     let backend = DevRantBackend()
     
     public init(requestLogger: Logger) {
-        self.request = Request(encoder: .devRant, decoder: .devRant, logger: requestLogger)
+        self.request = KreeRequest(encoder: .devRant, decoder: .devRant, logger: requestLogger)
     }
     
-    private func makeConfig(_ method: Request.Method, path: String, urlParameters: [String: String] = [:], headers: [String: String] = [:], token: AuthToken? = nil) -> Request.Config {
+    private func makeConfig(_ method: KreeRequest.Method, path: String, urlParameters: [String: String] = [:], headers: [String: String] = [:], token: AuthToken? = nil) -> KreeRequest.Config {
         var urlParameters = urlParameters
         urlParameters["app"] = "3"
         
@@ -24,7 +25,7 @@ public struct SwiftDevRant {
         return .init(method: method, backend: backend, path: path, urlParameters: urlParameters, headers: headers)
     }
     
-    private func makeMultipartConfig(_ method: Request.Method, path: String, parameters: [String: String] = [:], boundary: String, headers: [String: String] = [:], token: AuthToken? = nil) -> Request.Config {
+    private func makeMultipartConfig(_ method: KreeRequest.Method, path: String, parameters: [String: String] = [:], boundary: String, headers: [String: String] = [:], token: AuthToken? = nil) -> KreeRequest.Config {
         var parameters = parameters
         parameters["app"] = "3"
         
@@ -76,7 +77,7 @@ public extension SwiftDevRant {
         let config = makeConfig(.post, path: "users/auth-token")
         
         // For the log in request the url encoded parameters are passed as a string in the http body instead of in the URL.
-        let body = String(Request.urlEncodedQueryString(from: parameters).dropFirst()) // dropping the first character "?"
+        let body = String(KreeRequest.urlEncodedQueryString(from: parameters).dropFirst()) // dropping the first character "?"
         
         let response: AuthToken.CodingData.Container = try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)
         
@@ -492,7 +493,7 @@ public extension SwiftDevRant {
     ///    - userId: The id of the user to subscribe to or to unsubscribe from.
     ///    - subscribe: `true` subscribes to the user, `false` unsubscribes from the user.
     func subscribeToUser(token: AuthToken, userId: Int, subscribe: Bool) async throws {
-        let method: Request.Method = subscribe ? .post : .delete
+        let method: KreeRequest.Method = subscribe ? .post : .delete
         
         let config = makeConfig(method, path: "users/\(userId)/subscribe", token: token)