diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index c98b182..88264cd 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -7,12 +7,12 @@ import Foundation -protocol SomeCall : Storable { +protocol SomeCall: Storable { // func execute() throws var lastAttemptDate: Date { get } } -class ApiCall : ModelObject, Storable, SomeCall { +class ApiCall: ModelObject, Storable, SomeCall { static func resourceName() -> String { return "apicalls_" + T.resourceName() } diff --git a/LeStorage/MicroStorage.swift b/LeStorage/MicroStorage.swift index c3a40f6..6f09975 100644 --- a/LeStorage/MicroStorage.swift +++ b/LeStorage/MicroStorage.swift @@ -7,19 +7,19 @@ import Foundation -protocol MicroStorable : Codable { +public protocol MicroStorable : Codable { init() static var fileName: String { get } } -class MicroStorage { +public class MicroStorage { fileprivate(set) var item: T init() { var instance: T? = nil do { - let url = try FileUtils.directoryURLForFileName(T.fileName) + let url = try FileUtils.documentDirectoryURLForFileName(T.fileName) if FileManager.default.fileExists(atPath: url.absoluteString) { let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName) if let decoded: T = try jsonString.decode() { @@ -48,3 +48,50 @@ class MicroStorage { } } + +public class OptionalStorage { + + public var item: T? = nil { + didSet { + self._write() + } + } + + fileprivate var fileName: String + + public init(fileName: String) { + self.fileName = fileName + do { + let url = try FileUtils.documentDirectoryURLForFileName(fileName) + if FileManager.default.fileExists(atPath: url.path) { + let jsonString = try FileUtils.readDocumentFile(fileName: fileName) + if let decoded: T = try jsonString.decode() { + self.item = decoded + Logger.log("user loaded with: \(jsonString)") + } + } + } catch { + Logger.error(error) + } + } + + fileprivate func _write() { + + var content = "" + if let item = self.item { + do { + content = try item.jsonString() + } catch { + Logger.error(error) + } + } + + do { + let _ = try FileUtils.writeToDocumentDirectory(content: content, fileName: self.fileName) + } catch { + Logger.error(error) + } + + } + +} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index ea8e31b..6dc6780 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -18,6 +18,38 @@ enum ServiceError: Error { case urlCreationError(url: String) case cantConvertToUUID(id: String) case missingUserName + case responseError(response: String) +} + +fileprivate enum ServiceConf: String { + case createAccount = "users/" + case requestToken = "plus/api-token-auth/" + case getUser = "plus/user-by-token/" + case changePassword = "plus/change-password/" + + var method: Method { + switch self { + case .createAccount, .requestToken: + return .post + case .changePassword: + return .put + default: + return .get + } + } + + var requiresToken: Bool? { + switch self { + case .createAccount, .requestToken: + return false + case .getUser, .changePassword: + return true + default: + return nil + } + } + + } public class Services { @@ -49,15 +81,23 @@ public class Services { // MARK: - Base - fileprivate func _runRequest(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U { + fileprivate func _runRequest(serviceConf: ServiceConf, payload: T, apiCallId: String? = nil) async throws -> U { + var request = try self._baseRequest(conf: serviceConf) + request.httpBody = try jsonEncoder.encode(payload) + return try await _runRequest(request, apiCallId: apiCallId) + } + + fileprivate func _runRequest(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U { var request = try self._baseRequest(servicePath: servicePath, method: method) request.httpBody = try jsonEncoder.encode(payload) return try await _runRequest(request, apiCallId: apiCallId) } - fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { + fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) + Logger.log("response = \(String(describing: String(data: task.0, encoding: .utf8)))") + if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode Logger.log("request ended with status code = \(statusCode)") @@ -71,10 +111,10 @@ public class Services { if let apiCallId, let type = (T.self as? any Storable.Type) { try Store.main.rescheduleApiCall(id: apiCallId, type: type) } + let dataString = String(describing: String(data: task.0, encoding: .utf8)) + throw ServiceError.responseError(response: dataString) } } - - Logger.log("response = \(String(describing: String(data: task.0, encoding: .utf8)))") return try jsonDecoder.decode(T.self, from: task.0) } @@ -82,7 +122,11 @@ public class Services { return try self._baseRequest(servicePath: servicePath, method: .get) } - fileprivate func _baseRequest(servicePath: String, method: Method) throws -> URLRequest { + fileprivate func _baseRequest(conf: ServiceConf) throws -> URLRequest { + return try self._baseRequest(servicePath: conf.rawValue, method: conf.method, requiresToken: conf.requiresToken) + } + + fileprivate func _baseRequest(servicePath: String, method: Method, requiresToken: Bool? = nil) throws -> URLRequest { let urlString = baseURL + servicePath guard let url = URL(string: urlString) else { throw ServiceError.urlCreationError(url: urlString) @@ -90,7 +134,8 @@ public class Services { var request = URLRequest(url: url) request.httpMethod = method.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let token = try? self.keychainStore.getToken() { + if !(requiresToken == false), let token = try? self.keychainStore.getToken() { + Logger.log("current token = \(token)") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") } @@ -99,17 +144,17 @@ public class Services { // MARK: - Services - func get() async throws -> [T] { + func get() async throws -> [T] { let getRequest = try getRequest(servicePath: T.resourceName() + "/") return try await self._runRequest(getRequest) } - func runApiCall(_ apiCall: ApiCall) async throws -> T { + func runApiCall(_ apiCall: ApiCall) async throws -> T { let request = try self._request(from: apiCall) return try await self._runRequest(request, apiCallId: apiCall.id) } - fileprivate func _request(from apiCall: ApiCall) throws -> URLRequest { + fileprivate func _request(from apiCall: ApiCall) throws -> URLRequest { guard let url = URL(string: apiCall.url) else { throw ServiceError.urlCreationError(url: apiCall.url) } @@ -130,9 +175,9 @@ public class Services { // MARK: - Authentication - public func createAccount(user: U) async throws -> V { + public func createAccount(user: U) async throws -> V { - let response: V = try await _runRequest(servicePath: "users/", method: .post, payload: user) + let response: V = try await _runRequest(serviceConf: .createAccount, payload: user) // var postRequest = try self._baseRequest(servicePath: "users/", method: .post) // postRequest.httpBody = try jsonEncoder.encode(user) @@ -143,7 +188,7 @@ public class Services { } func requestToken(username: String, password: String) async throws -> String { - var postRequest = try self._baseRequest(servicePath: "plus/api-token-auth/", method: .post) + var postRequest = try self._baseRequest(conf: .requestToken) let credentials = Credentials(username: username, password: password) postRequest.httpBody = try jsonEncoder.encode(credentials) let response: AuthResponse = try await self._runRequest(postRequest) @@ -153,42 +198,37 @@ public class Services { fileprivate func _storeToken(username: String, token: String) { do { + try self.keychainStore.deleteToken() try self.keychainStore.add(username: username, token: token) } catch { Logger.error(error) } } - public func login(username: String, password: String) async throws -> U { - let token: String = try await requestToken(username: username, password: password) - Logger.log("token = \(token)") - var postRequest = try self._baseRequest(servicePath: "plus/user-by-token/", method: .post) - postRequest.httpBody = try jsonEncoder.encode(Token(token: token)) + public func login(username: String, password: String) async throws -> U { + _ = try await requestToken(username: username, password: password) + let postRequest = try self._baseRequest(conf: .getUser) let user: U = try await self._runRequest(postRequest) - Logger.log("user = \(user.username), id = \(user.id)") Store.main.setUserUUID(uuidString: user.id) + Store.main.setUserName(user.username) return user } - public func changePassword(password1: String, password2: String) async throws { + public func changePassword(oldPassword: String, password1: String, password2: String) async throws { guard let username = Store.main.userName() else { throw ServiceError.missingUserName } struct ChangePasswordParams: Codable { + var old_password: String var new_password1: String var new_password2: String } - let params = ChangePasswordParams(new_password1: password1, new_password2: password2) - let response: Token = try await self._runRequest( - servicePath: "plus/change-password/", method: .post, payload: params) - -// var postRequest = try self._baseRequest(servicePath: "plus/change-password/", method: .post) -// postRequest.httpBody = try jsonEncoder.encode(params) -// let response: Token = try await self._runRequest(postRequest) - + let params = ChangePasswordParams(old_password: oldPassword, new_password1: password1, new_password2: password2) + let response: Token = try await self._runRequest(serviceConf: .changePassword, payload: params) + self._storeToken(username: username, token: response.token) } @@ -220,7 +260,7 @@ struct Token: Codable { var token: String } -public protocol UserBase : Codable { +public protocol UserBase: Codable { var id: String { get } var username: String { get } // var password: String? { get } diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index a544943..de444c1 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -7,7 +7,7 @@ import Foundation -public protocol Storable : Codable, Identifiable where ID : StringProtocol { +public protocol Storable: Codable, Identifiable where ID : StringProtocol { static func resourceName() -> String func deleteDependencies() throws } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 263b676..3814e45 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -80,11 +80,6 @@ public class Store { return collection } - func setupServer(url: String, userModel: T) { - self.synchronizationApiURL = url - - } - // MARK: - Settings func setUserUUID(uuidString: String) { @@ -131,7 +126,7 @@ public class Store { // MARK: - Convenience /// Looks for an instance by id - public func findById(_ id: String) -> T? { + public func findById(_ id: String) -> T? { guard let collection = self._collections[T.resourceName()] as? StoredCollection else { Logger.w("Collection \(T.resourceName()) not registered") return nil @@ -140,7 +135,7 @@ public class Store { } /// Filters a collection with a [isIncluded] predicate - public func filter(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { + public func filter(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { do { return try self.collection().filter(isIncluded) } catch { @@ -149,7 +144,7 @@ public class Store { } /// Returns a collection by type - func collection() throws -> StoredCollection { + func collection() throws -> StoredCollection { if let collection = self._collections[T.resourceName()] as? StoredCollection { return collection } @@ -157,7 +152,7 @@ public class Store { } /// Deletes the dependencies of a collection - public func deleteDependencies(items: any Sequence) throws { + public func deleteDependencies(items: any Sequence) throws { try self.collection().deleteDependencies(items) } @@ -169,7 +164,7 @@ public class Store { } /// [beta] Performs the migration if necessary - func performMigrationIfNecessary(_ collection: StoredCollection) async throws { + func performMigrationIfNecessary(_ collection: StoredCollection) async throws { // Check for migrations let migrations = self._migrations.filter { $0.resourceName == T.resourceName() } @@ -213,13 +208,13 @@ public class Store { } /// Reschedule an ApiCall by id - func rescheduleApiCall(id: String, type: T.Type) throws { + func rescheduleApiCall(id: String, type: T.Type) throws { let collection: StoredCollection = try self.collection() collection.rescheduleApiCallsIfNecessary() } /// Executes an ApiCall - fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { + fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { guard let service else { throw StoreError.missingService } @@ -236,7 +231,7 @@ public class Store { } /// Retrieves all the items on the server - func getItems() async throws -> [T] { + func getItems() async throws -> [T] { guard let service else { throw StoreError.missingService } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 688fb46..f3155c9 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -7,13 +7,13 @@ import Foundation -enum StoredCollectionError : Error { +enum StoredCollectionError: Error { case unmanagedHTTPMethod(method: String) case missingApiCallCollection case missingInstance } -protocol SomeCollection : Identifiable { +protocol SomeCollection: Identifiable { func allItems() -> [any Storable] func deleteById(_ id: String) throws func deleteApiCallById(_ id: String) throws @@ -24,7 +24,7 @@ extension Notification.Name { public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange") } -public class StoredCollection : RandomAccessCollection, SomeCollection { +public class StoredCollection: RandomAccessCollection, SomeCollection { /// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL let synchronized: Bool @@ -82,7 +82,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec /// Migrates if necessary and asynchronously decodes the json file fileprivate func _load() { do { - let url = try FileUtils.directoryURLForFileName(T.fileName()) + let url = try FileUtils.documentDirectoryURLForFileName(T.fileName()) if FileManager.default.fileExists(atPath: url.path()) { if self.asynchronousIO { diff --git a/LeStorage/Utils/FileUtils.swift b/LeStorage/Utils/FileUtils.swift index d86fcbc..d9b1ddc 100644 --- a/LeStorage/Utils/FileUtils.swift +++ b/LeStorage/Utils/FileUtils.swift @@ -19,7 +19,7 @@ class FileUtils { } static func readDocumentFile(fileName: String) throws -> String { - let fileURL: URL = try self.directoryURLForFileName(fileName) + let fileURL: URL = try self.documentDirectoryURLForFileName(fileName) // Logger.log("url = \(fileURL.absoluteString)") return try String(contentsOf: fileURL, encoding: .utf8) @@ -35,7 +35,7 @@ class FileUtils { return try String(contentsOf: fileURL, encoding: .utf8) } - static func directoryURLForFileName(_ fileName: String) throws -> URL { + static func documentDirectoryURLForFileName(_ fileName: String) throws -> URL { if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { return dir.appendingPathComponent(fileName) }