From d782a2fcf51ac5e64c97e7e2ad870bf4acbf3c25 Mon Sep 17 00:00:00 2001 From: Wilhelm Oks 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: 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(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.noInternet - } else { - throw Error.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.notFound - } else { - throw Error.apiError(try decoder.decode(apiError, from: data)) - } - } else { - throw Error.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(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(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(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(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(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(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(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)