Adds documentation to the methods

multistore
Laurent 1 year ago
parent 72f4345c1f
commit e965e72e9a
  1. 1
      LeStorage/ModelObject.swift
  2. 83
      LeStorage/Services.swift
  3. 17
      LeStorage/Storable.swift
  4. 16
      LeStorage/Store.swift

@ -7,6 +7,7 @@
import Foundation
/// A class used as the root class for Storable objects
open class ModelObject {
public init() { }

@ -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<T: Encodable, U: Decodable>(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<T: Decodable>(_ 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<T : Storable>(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<T: Storable>(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<T: Storable>(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<T: Storable>(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<T: Storable>(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<T: Storable>() async throws -> [T] {
let getRequest = try _getRequest(type: T.self)
return try await self._runRequest(getRequest)
}
/// Executes a POST request
public func post<T: Storable>(_ 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<T: Storable>(_ 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<T: Storable>(_ apiCall: ApiCall<T>) 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<T: Storable>(from apiCall: ApiCall<T>) 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<T: Storable>(from apiCall: ApiCall<T>) 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<U: UserPasswordBase, V: UserBase>(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<U: UserBase>(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

@ -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<T : Storable>(_ 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 {

@ -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
}

Loading…
Cancel
Save