From e965e72e9afa3671b7c3471544e4d9a83b347c22 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 3 Jun 2024 16:52:50 +0200 Subject: [PATCH] Adds documentation to the methods --- LeStorage/ModelObject.swift | 1 + LeStorage/Services.swift | 83 +++++++++++++++++++++++++++++++++---- LeStorage/Storable.swift | 17 ++++++-- LeStorage/Store.swift | 16 ++++++- 4 files changed, 103 insertions(+), 14 deletions(-) diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index 9f1939b..0497c01 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -7,6 +7,7 @@ import Foundation +/// A class used as the root class for Storable objects open class ModelObject { public init() { } diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 8edfd51..216a8b9 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -19,7 +19,6 @@ public enum ServiceError: Error { case cantConvertToUUID(id: String) case missingUserName case responseError(response: String) -// case missingToken } fileprivate enum ServiceConf: String { @@ -52,8 +51,10 @@ fileprivate enum ServiceConf: String { } +/// A class used to send HTTP request to the django server public class Services { + /// A KeychainStore object used to store the user's token let keychainStore: KeychainStore public init(url: String) { @@ -81,12 +82,21 @@ public class Services { // MARK: - Base + /// Runs a request using a configuration object + /// - Parameters: + /// - serviceConf: A instance of ServiceConf + /// - payload: a codable value stored in the body of the request + /// - apiCallId: an optional id referencing an ApiCall 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) } + /// Runs a request using a traditional URLRequest + /// - Parameters: + /// - request: the URLRequest to run + /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure 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) @@ -110,7 +120,6 @@ public class Services { errorString = message } - if let apiCallId, let type = (T.self as? any Storable.Type) { try Store.main.rescheduleApiCall(id: apiCallId, type: type) Store.main.logFailedAPICall(apiCallId, collectionName: type.resourceName(), error: errorString) @@ -122,6 +131,10 @@ public class Services { return try jsonDecoder.decode(T.self, from: task.0) } + /// Returns if the token is required for a request + /// - Parameters: + /// - type: the type of the request resource + /// - method: the HTTP method of the request fileprivate func _isTokenRequired(type: T.Type, method: HTTPMethod) -> Bool { let methods = T.tokenExemptedMethods() if methods.contains(method) { @@ -131,30 +144,50 @@ public class Services { } } + /// Returns a GET request for the resource + /// - Parameters: + /// - type: the type of the request resource fileprivate func _getRequest(type: T.Type) throws -> URLRequest { let requiresToken = self._isTokenRequired(type: T.self, method: .get) return try self._baseRequest(servicePath: T.path(), method: .get, requiresToken: requiresToken) } + /// Returns a POST request for the resource + /// - Parameters: + /// - type: the type of the request resource fileprivate func _postRequest(type: T.Type) throws -> URLRequest { let requiresToken = self._isTokenRequired(type: T.self, method: .post) return try self._baseRequest(servicePath: T.path(), method: .post, requiresToken: requiresToken) } + /// Returns a PUT request for the resource + /// - Parameters: + /// - type: the type of the request resource fileprivate func _putRequest(type: T.Type, id: String) throws -> URLRequest { let requiresToken = self._isTokenRequired(type: T.self, method: .put) return try self._baseRequest(servicePath: T.path(id: id), method: .put, requiresToken: requiresToken) } + /// Returns a DELETE request for the resource + /// - Parameters: + /// - type: the type of the request resource fileprivate func _deleteRequest(type: T.Type, id: String) throws -> URLRequest { let requiresToken = self._isTokenRequired(type: T.self, method: .delete) return try self._baseRequest(servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken) } + /// Returns the base URLRequest for a ServiceConf instance + /// - Parameters: + /// - conf: a ServiceConf instance fileprivate func _baseRequest(conf: ServiceConf) throws -> URLRequest { return try self._baseRequest(servicePath: conf.rawValue, method: conf.method, requiresToken: conf.requiresToken) } + /// Returns a base request for a path and method + /// - Parameters: + /// - servicePath: the path to add to the API base URL + /// - method: the HTTP method to execute + /// - requiresToken: An optional boolean to indicate if the token is required fileprivate func _baseRequest(servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil) throws -> URLRequest { let urlString = baseURL + servicePath guard let url = URL(string: urlString) else { @@ -173,28 +206,35 @@ public class Services { // MARK: - Services + /// Executes a GET request public func get() async throws -> [T] { let getRequest = try _getRequest(type: T.self) return try await self._runRequest(getRequest) } - + + /// Executes a POST request public func post(_ instance: T) async throws -> T { var postRequest = try self._postRequest(type: T.self) postRequest.httpBody = try jsonEncoder.encode(instance) return try await self._runRequest(postRequest) } + /// Executes a PUT request public func put(_ instance: T) async throws -> T { var postRequest = try self._putRequest(type: T.self, id: instance.stringId) postRequest.httpBody = try jsonEncoder.encode(instance) return try await self._runRequest(postRequest) } + /// Executes an ApiCall func runApiCall(_ apiCall: ApiCall) async throws -> T { let request = try self._request(from: apiCall) return try await self._runRequest(request, apiCallId: apiCall.id) } + /// Returns the URLRequest for an ApiCall + /// - Parameters: + /// - apiCall: An ApiCall instance to configure the returned request fileprivate func _request(from apiCall: ApiCall) throws -> URLRequest { let url = try self._url(from: apiCall) var request = URLRequest(url: url) @@ -214,6 +254,9 @@ public class Services { return request } + /// Returns the URL corresponding to the ApiCall + /// - Parameters: + /// - apiCall: an instance of ApiCall to build to URL fileprivate func _url(from apiCall: ApiCall) throws -> URL { var stringURL: String = self.baseURL switch apiCall.method { @@ -231,14 +274,19 @@ public class Services { // MARK: - Authentication + /// Creates an account + /// - Parameters: + /// - user: A user instance to send to the server public func createAccount(user: U) async throws -> V { - let response: V = try await _runRequest(serviceConf: .createAccount, payload: user) -// let _ = try await requestToken(username: user.username, password: user.password) Store.main.setUserName(user.username) return response } + /// Requests a token for a username and password + /// - Parameters: + /// - username: the account's username + /// - password: the account's password public func requestToken(username: String, password: String) async throws -> String { var postRequest = try self._baseRequest(conf: .requestToken) let credentials = Credentials(username: username, password: password) @@ -248,6 +296,10 @@ public class Services { return response.token } + /// Stores a token for a corresponding username + /// - Parameters: + /// - username: the key used to store the token + /// - token: the token to store fileprivate func _storeToken(username: String, token: String) { do { try self.keychainStore.deleteToken() @@ -257,6 +309,10 @@ public class Services { } } + /// A login method that actually requests a token from the server, and stores the appropriate data for later usage + /// - Parameters: + /// - username: the account's username + /// - password: the account's password public func login(username: String, password: String) async throws -> U { _ = try await requestToken(username: username, password: password) let postRequest = try self._baseRequest(conf: .getUser) @@ -266,6 +322,11 @@ public class Services { return user } + /// A method that sends a request to change a user's password + /// - Parameters: + /// - oldPassword: the account's old password + /// - password1: the account's new password + /// - password2: a repeat of the new password public func changePassword(oldPassword: String, password1: String, password2: String) async throws { guard let username = Store.main.userName() else { @@ -284,6 +345,9 @@ public class Services { self._storeToken(username: username, token: response.token) } + /// The method send a request to reset the user's password + /// - Parameters: + /// - email: the email of the user public func forgotPassword(email: String) async throws { var postRequest = try self._baseRequest(servicePath: "dj-rest-auth/password/reset/", method: .post) postRequest.httpBody = try jsonEncoder.encode(Email(email: email)) @@ -291,17 +355,21 @@ public class Services { Logger.log("response = \(response)") } - func disconnect() throws { + /// Deletes the locally stored token + func deleteToken() throws { try self.keychainStore.deleteToken() } + /// Parse a json data and tries to extract its error message + /// - Parameters: + /// - data: some JSON data fileprivate func errorMessageFromResponse(data: Data) -> String? { do { if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let stringsArray = jsonObject.values.first as? [String] { return stringsArray.first } } catch { - print("Failed to parse JSON: \(error.localizedDescription)") + Logger.log("Failed to parse JSON: \(error.localizedDescription)") } return nil } @@ -311,7 +379,6 @@ public class Services { struct AuthResponse: Codable { let token: String } - struct Credentials: Codable { var username: String var password: String diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 9e063ec..0ebb76c 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -7,25 +7,34 @@ import Foundation +/// A protocol describing classes that can be stored locally in JSON and synchronized on our django server public protocol Storable: Codable, Identifiable where ID : StringProtocol { + + /// The resource name corresponding to the resource path on the API + /// Also used as the name of the local file static func resourceName() -> String + + /// Returns HTTP methods that do not need to pass the token to the request static func tokenExemptedMethods() -> [HTTPMethod] + /// A method that deletes the local dependencies of the resource + /// Mimics the behavior the cascading delete on the django server + /// Typically when we delete a resource, we automatically delete items that depends on it, + /// so when we do that on the server, we also need to do it locally func deleteDependencies() throws } extension Storable { - public func findById(_ id: String) -> T? { - return Store.main.findById(id) - } - + /// Returns a filename for the class type static func fileName() -> String { return self.resourceName() + ".json" } + /// Returns a string id for the instance var stringId: String { return String(self.id) } + /// Returns the relative path of the instance for the django server static func path(id: String? = nil) -> String { var path = self.resourceName() + "/" if let id { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 60d1c7c..cbc5aa7 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -44,6 +44,7 @@ public class Store { /// The services performing the API calls fileprivate var _services: Services? + /// Returns the service instance public func service() throws -> Services { if let service = self._services { return service @@ -105,6 +106,7 @@ public class Store { } } + /// Returns the stored user Id public var userId: String? { return self._settingsStorage.item.userId } @@ -121,13 +123,14 @@ public class Store { } } + /// Returns the stored token public func token() -> String? { return try? self.service().keychainStore.getToken() } /// Disconnect the user from the storage and resets collection public func disconnect(resetOption: ResetOption? = nil) { - try? self.service().disconnect() + try? self.service().deleteToken() self._settingsStorage.update { settings in settings.username = nil settings.userId = nil @@ -237,6 +240,7 @@ public class Store { } } + /// Resets all the api call collections public func resetApiCalls() { for collection in self._collections.values { collection.resetApiCalls() @@ -257,21 +261,29 @@ public class Store { return self._collections.values.contains(where: { $0.hasPendingAPICalls() }) } + /// Returns the names of all collections public func collectionNames() -> [String] { return self._collections.values.map { $0.resourceName } } + /// Returns the content of the api call file public func apiCallsFile(resourceName: String) -> String { return self._collections[resourceName]?.contentOfApiCallFile() ?? "" } + /// This method triggers the framework to save and send failed api calls public func logsFailedAPICalls() { self._failedAPICallsCollection = self.registerCollection(synchronized: true) } + /// If configured for, logs and send to the server a failed API call + /// Logs a failed API call that has failed at least 5 times func logFailedAPICall(_ apiCallId: String, collectionName: String, error: String) { - guard let failedAPICallsCollection = self._failedAPICallsCollection, let collection = self._collections[collectionName], let apiCall = try? collection.apiCallById(apiCallId), let userId = Store.main.userId else { + guard let failedAPICallsCollection = self._failedAPICallsCollection, + let collection = self._collections[collectionName], + let apiCall = try? collection.apiCallById(apiCallId), + let userId = Store.main.userId else { return }