From ce6de4d61f174c982dc87f98ece7b87cb8f9c302 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Thu, 9 Jan 2025 10:43:02 +0100 Subject: [PATCH] Prettify stuff --- .../APIClient/APIClient+Logging.swift | 44 ++++++++++++ .../APIClient/APIClient+URLSession.swift | 69 ++++++++++++++++++ .../BSWFoundation/APIClient/APIClient.swift | 71 ------------------- Sources/BSWFoundation/APIClient/Router.swift | 13 ++-- .../Extensions/Collections.swift | 16 ----- Sources/BSWFoundation/Extensions/Task.swift | 1 - Sources/BSWFoundation/ModuleConstants.swift | 2 - Sources/BSWFoundation/Parse/JSONParser.swift | 2 +- .../CodableKeychainBacked.swift | 47 ------------ .../PropertyWrappers/KeychainBacked.swift | 42 +++++++++++ .../PropertyWrappers/UserDefaultsBacked.swift | 13 ++-- 11 files changed, 171 insertions(+), 149 deletions(-) create mode 100644 Sources/BSWFoundation/APIClient/APIClient+Logging.swift create mode 100644 Sources/BSWFoundation/APIClient/APIClient+URLSession.swift delete mode 100644 Sources/BSWFoundation/PropertyWrappers/CodableKeychainBacked.swift diff --git a/Sources/BSWFoundation/APIClient/APIClient+Logging.swift b/Sources/BSWFoundation/APIClient/APIClient+Logging.swift new file mode 100644 index 0000000..0878cbf --- /dev/null +++ b/Sources/BSWFoundation/APIClient/APIClient+Logging.swift @@ -0,0 +1,44 @@ +import Foundation +import OSLog + +//MARK: Logging + +extension APIClient { + + func logRequest(request: URLRequest) { + let logger = Logger(subsystem: submoduleName("APIClient"), category: "APIClient.Request") + switch loggingConfiguration.requestBehaviour { + case .all: + let httpMethod = request.httpMethod ?? "GET" + let path = request.url?.path ?? "" + logger.debug("Method: \(httpMethod) Path: \(path)") + if let data = request.httpBody, let prettyString = String(data: data, encoding: .utf8) { + logger.debug("Body: \(prettyString)") + } + default: + break + } + } + + func logResponse(_ response: Response) { + let logger = Logger(subsystem: submoduleName("APIClient"), category: "APIClient.Response") + let isError = !(200..<300).contains(response.httpResponse.statusCode) + let shouldLogThis: Bool = { + switch loggingConfiguration.responseBehaviour { + case .all: + return true + case .none: + return false + case .onlyFailing: + return isError + } + }() + guard shouldLogThis else { return } + let logType: OSLogType = isError ? .error : .debug + let path = response.httpResponse.url?.path ?? "" + logger.log(level: logType, "StatusCode: \(response.httpResponse.statusCode) Path: \(path)") + if isError, let errorString = String(data: response.data, encoding: .utf8) { + logger.log(level: logType, "Error Message: \(errorString)") + } + } +} diff --git a/Sources/BSWFoundation/APIClient/APIClient+URLSession.swift b/Sources/BSWFoundation/APIClient/APIClient+URLSession.swift new file mode 100644 index 0000000..55c2d03 --- /dev/null +++ b/Sources/BSWFoundation/APIClient/APIClient+URLSession.swift @@ -0,0 +1,69 @@ +// +// Created by Pierluigi Cifani on 8/1/25. +// + +import Foundation + +//MARK: APIClientNetworkFetcher + +extension URLSession: APIClientNetworkFetcher { + + public func fetchData(with urlRequest: URLRequest) async throws -> APIClient.Response { + let tuple = try await data(for: urlRequest) + guard let httpResponse = tuple.1 as? HTTPURLResponse else { + throw APIClient.Error.malformedResponse + } + return .init(data: tuple.0, httpResponse: httpResponse) + } + + public func uploadFile(with urlRequest: URLRequest, fileURL: URL) async throws -> APIClient.Response { + let task = Task.detached { + try await self.upload(for: urlRequest, fromFile: fileURL) + } + let cancelTask: @Sendable () -> () = { + task.cancel() + } + let wrapper = APIClient.ApplicationWrapper() + let backgroundTaskID = await wrapper.generateBackgroundTaskID(cancelTask: cancelTask) + let (data, response) = try await task.value + await wrapper.endBackgroundTask(id: backgroundTaskID) + guard let httpResponse = response as? HTTPURLResponse else { + throw APIClient.Error.malformedResponse + } + return .init(data: data, httpResponse: httpResponse) + } +} + +public typealias HTTPHeaders = [String: String] +public struct VoidResponse: Decodable, Hashable, Sendable {} + +// MARK: UIApplicationWrapper +/// This is here just to make sure that on non-UIKit +/// platforms we have a nice API to call to. +#if canImport(UIKit) +import UIKit +private extension APIClient { + class ApplicationWrapper { + func generateBackgroundTaskID(cancelTask: @escaping (@MainActor @Sendable () -> Void)) async -> UIBackgroundTaskIdentifier { + return await UIApplication.shared.beginBackgroundTask(expirationHandler: cancelTask) + } + + func endBackgroundTask(id: UIBackgroundTaskIdentifier) async { + await UIApplication.shared.endBackgroundTask(id) + } + } +} +#else +private extension APIClient { + class ApplicationWrapper { + func generateBackgroundTaskID(cancelTask: @escaping (@MainActor @Sendable () -> Void)) async -> Int { + return 0 + } + + func endBackgroundTask(id: Int) async { + + } + } +} +#endif + diff --git a/Sources/BSWFoundation/APIClient/APIClient.swift b/Sources/BSWFoundation/APIClient/APIClient.swift index 599d96f..fd3a070 100644 --- a/Sources/BSWFoundation/APIClient/APIClient.swift +++ b/Sources/BSWFoundation/APIClient/APIClient.swift @@ -238,77 +238,6 @@ private extension APIClient { } } -import OSLog - -//MARK: Logging - -private extension APIClient { - private func logRequest(request: URLRequest) { - let logger = Logger(subsystem: submoduleName("APIClient"), category: "APIClient.Request") - switch loggingConfiguration.requestBehaviour { - case .all: - let httpMethod = request.httpMethod ?? "GET" - let path = request.url?.path ?? "" - logger.debug("Method: \(httpMethod) Path: \(path)") - if let data = request.httpBody, let prettyString = String(data: data, encoding: .utf8) { - logger.debug("Body: \(prettyString)") - } - default: - break - } - } - - private func logResponse(_ response: Response) { - let logger = Logger(subsystem: submoduleName("APIClient"), category: "APIClient.Response") - let isError = !(200..<300).contains(response.httpResponse.statusCode) - let shouldLogThis: Bool = { - switch loggingConfiguration.responseBehaviour { - case .all: - return true - case .none: - return false - case .onlyFailing: - return isError - } - }() - guard shouldLogThis else { return } - let path = response.httpResponse.url?.path ?? "" - logger.debug("StatusCode: \(response.httpResponse.statusCode) Path: \(path)") - if isError, let errorString = String(data: response.data, encoding: .utf8) { - logger.debug("Error Message: \(errorString)") - } - } -} - -extension URLSession: APIClientNetworkFetcher { - - public func fetchData(with urlRequest: URLRequest) async throws -> APIClient.Response { - let tuple = try await self.data(for: urlRequest) - guard let httpResponse = tuple.1 as? HTTPURLResponse else { - throw APIClient.Error.malformedResponse - } - return .init(data: tuple.0, httpResponse: httpResponse) - } - - public func uploadFile(with urlRequest: URLRequest, fileURL: URL) async throws -> APIClient.Response { - let cancelTask: @Sendable () -> () = {} -#if os(iOS) - let backgroundTask = await UIApplication.shared.beginBackgroundTask(expirationHandler: cancelTask) -#endif - let (data, response) = try await self.upload(for: urlRequest, fromFile: fileURL) -#if os(iOS) - await UIApplication.shared.endBackgroundTask(backgroundTask) -#endif - guard let httpResponse = response as? HTTPURLResponse else { - throw APIClient.Error.malformedResponse - } - return .init(data: data, httpResponse: httpResponse) - } -} - -public typealias HTTPHeaders = [String: String] -public struct VoidResponse: Decodable, Hashable, Sendable {} - private extension Swift.Error { var is401: Bool { guard diff --git a/Sources/BSWFoundation/APIClient/Router.swift b/Sources/BSWFoundation/APIClient/Router.swift index 460cd41..4ed0147 100644 --- a/Sources/BSWFoundation/APIClient/Router.swift +++ b/Sources/BSWFoundation/APIClient/Router.swift @@ -70,24 +70,27 @@ private enum URLEncoding { 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] { + } + else if let array = value as? [Any] { for value in array { components += queryComponents(fromKey: "\(key)[]", value: value) } - } else if let value = value as? NSNumber { + } + else if let value = value as? NSNumber { if value.isBool { components.append((escape(key), escape((value.boolValue ? "1" : "0")))) } else { components.append((escape(key), escape("\(value)"))) } - } else if let bool = value as? Bool { + } + else if let bool = value as? Bool { components.append((escape(key), escape((bool ? "1" : "0")))) - } else { + } + else { components.append((escape(key), escape("\(value)"))) } diff --git a/Sources/BSWFoundation/Extensions/Collections.swift b/Sources/BSWFoundation/Extensions/Collections.swift index d5a6027..42986c5 100644 --- a/Sources/BSWFoundation/Extensions/Collections.swift +++ b/Sources/BSWFoundation/Extensions/Collections.swift @@ -3,8 +3,6 @@ // Copyright © 2018 TheLeftBit SL SL. All rights reserved. // -import Foundation - public extension Sequence { func find(predicate: (Self.Iterator.Element) throws -> Bool) rethrows -> Self.Iterator.Element? { for element in self { @@ -30,20 +28,6 @@ public extension Collection { } } -public extension MutableCollection where Index == Int { - /// Shuffle the elements of `self` in-place. - mutating func shuffle() { - // empty and single-element collections don't shuffle - if count < 2 { return } - - for i in startIndex ..< endIndex - 1 { - let j = Int(arc4random_uniform(UInt32(endIndex - i))) + i - guard i != j else { continue } - self.swapAt(i, j) - } - } -} - public extension Array { mutating func moveItem(fromIndex oldIndex: Index, toIndex newIndex: Index) { insert(remove(at: oldIndex), at: newIndex) diff --git a/Sources/BSWFoundation/Extensions/Task.swift b/Sources/BSWFoundation/Extensions/Task.swift index 55c9b25..123e91a 100644 --- a/Sources/BSWFoundation/Extensions/Task.swift +++ b/Sources/BSWFoundation/Extensions/Task.swift @@ -1,4 +1,3 @@ -import Foundation public extension Task where Success == Never, Failure == Never { /// Returns a Task that will never return... Well, actually, it'll complete in 1000 seconds diff --git a/Sources/BSWFoundation/ModuleConstants.swift b/Sources/BSWFoundation/ModuleConstants.swift index 3a92067..9435041 100644 --- a/Sources/BSWFoundation/ModuleConstants.swift +++ b/Sources/BSWFoundation/ModuleConstants.swift @@ -3,8 +3,6 @@ // Copyright (c) 2016 TheLeftBit SL. All rights reserved. // -import Foundation - nonisolated func submoduleName(_ submodule : String) -> String { let ModuleName = "com.bswfoundation" return ModuleName + "." + submodule diff --git a/Sources/BSWFoundation/Parse/JSONParser.swift b/Sources/BSWFoundation/Parse/JSONParser.swift index af08677..1624be3 100644 --- a/Sources/BSWFoundation/Parse/JSONParser.swift +++ b/Sources/BSWFoundation/Parse/JSONParser.swift @@ -77,7 +77,7 @@ public enum JSONParser { throw Error.malformedSchema case .dataCorrupted(let context): print("*ERROR* Data Corrupted \"\(context)\")") - if let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { + if let string = String(data: data, encoding: .utf8) { print("*ERROR* incoming JSON: \(string)") } throw Error.malformedJSON diff --git a/Sources/BSWFoundation/PropertyWrappers/CodableKeychainBacked.swift b/Sources/BSWFoundation/PropertyWrappers/CodableKeychainBacked.swift deleted file mode 100644 index 162dd35..0000000 --- a/Sources/BSWFoundation/PropertyWrappers/CodableKeychainBacked.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Created by Michele Restuccia on 07/09/2020. -// - -import Foundation -import KeychainAccess - -/// Stores the given `T` type on the Keychain (as long as it's `Codable`) -@propertyWrapper -public class CodableKeychainBacked { - private let key: String - private let keychain = Keychain(service: Bundle.main.bundleIdentifier!) - - public init(key: String) { - self.key = key - } - - public var wrappedValue: T? { - get { - return keychain[key]?.decoded() - } set { - keychain[key] = newValue.encodedAsString() - } - } -} - -public extension CodableKeychainBacked { - func reset() { - wrappedValue = nil - } -} - -private extension String { - func decoded() -> T? { - guard let data = self.data(using: .utf8) else { return nil } - return try? JSONDecoder().decode(T.self, from: data) - } -} - -private extension Encodable { - func encodedAsString() -> String? { - guard let data = try? JSONEncoder().encode(self), let string = String(data: data, encoding: .utf8) else { - return nil - } - return string - } -} diff --git a/Sources/BSWFoundation/PropertyWrappers/KeychainBacked.swift b/Sources/BSWFoundation/PropertyWrappers/KeychainBacked.swift index a40c73c..7480fb1 100644 --- a/Sources/BSWFoundation/PropertyWrappers/KeychainBacked.swift +++ b/Sources/BSWFoundation/PropertyWrappers/KeychainBacked.swift @@ -36,3 +36,45 @@ public extension KeychainBacked { wrappedValue = nil } } + +/// Stores the given `T` type on the Keychain (as long as it's `Codable`) +@propertyWrapper +public class CodableKeychainBacked { + private let key: String + private let keychain: Keychain + + public init(key: String) { + self.key = key + self.keychain = Keychain(service: Bundle.main.bundleIdentifier!) + } + + public var wrappedValue: T? { + get { + return keychain[key]?.decoded() + } set { + keychain[key] = newValue.encodedAsString() + } + } +} + +public extension CodableKeychainBacked { + func reset() { + wrappedValue = nil + } +} + +private extension String { + func decoded() -> T? { + guard let data = self.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode(T.self, from: data) + } +} + +private extension Encodable { + func encodedAsString() -> String? { + guard let data = try? JSONEncoder().encode(self), let string = String(data: data, encoding: .utf8) else { + return nil + } + return string + } +} diff --git a/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift b/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift index 4f85c61..4bf7657 100644 --- a/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift +++ b/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift @@ -37,7 +37,7 @@ public final class UserDefaultsBacked: Sendable { } else { self.store.removeObject(forKey: key) } - self.store.synchronize() + _ = self.store.synchronize() } } } @@ -70,16 +70,17 @@ public final class CodableUserDefaultsBacked: Sendable { public var wrappedValue: T? { get { - guard let data = self.store.data(forKey: key) else { + guard let data = store.data(forKey: key) else { return defaultValue } return try? JSONDecoder().decode(T.self, from: data) } set { - guard let data = try? JSONEncoder().encode(newValue) else { - return + if let newValue, let data = try? JSONEncoder().encode(newValue) { + store.set(data, forKey: key) + } else { + store.set(nil, forKey: key) } - self.store.set(data, forKey: key) - self.store.synchronize() + _ = store.synchronize() } } }