Service fixes

multistore
Laurent 2 years ago
parent af4cdb36ea
commit dbe7de368a
  1. 4
      LeStorage/Codables/ApiCall.swift
  2. 53
      LeStorage/MicroStorage.swift
  3. 96
      LeStorage/Services.swift
  4. 2
      LeStorage/Storable.swift
  5. 21
      LeStorage/Store.swift
  6. 8
      LeStorage/StoredCollection.swift
  7. 4
      LeStorage/Utils/FileUtils.swift

@ -7,12 +7,12 @@
import Foundation import Foundation
protocol SomeCall : Storable { protocol SomeCall: Storable {
// func execute() throws // func execute() throws
var lastAttemptDate: Date { get } var lastAttemptDate: Date { get }
} }
class ApiCall<T : Storable> : ModelObject, Storable, SomeCall { class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
static func resourceName() -> String { return "apicalls_" + T.resourceName() } static func resourceName() -> String { return "apicalls_" + T.resourceName() }

@ -7,19 +7,19 @@
import Foundation import Foundation
protocol MicroStorable : Codable { public protocol MicroStorable : Codable {
init() init()
static var fileName: String { get } static var fileName: String { get }
} }
class MicroStorage<T : MicroStorable> { public class MicroStorage<T : MicroStorable> {
fileprivate(set) var item: T fileprivate(set) var item: T
init() { init() {
var instance: T? = nil var instance: T? = nil
do { do {
let url = try FileUtils.directoryURLForFileName(T.fileName) let url = try FileUtils.documentDirectoryURLForFileName(T.fileName)
if FileManager.default.fileExists(atPath: url.absoluteString) { if FileManager.default.fileExists(atPath: url.absoluteString) {
let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName) let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName)
if let decoded: T = try jsonString.decode() { if let decoded: T = try jsonString.decode() {
@ -48,3 +48,50 @@ class MicroStorage<T : MicroStorable> {
} }
} }
public class OptionalStorage<T : Codable> {
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)
}
}
}

@ -18,6 +18,38 @@ enum ServiceError: Error {
case urlCreationError(url: String) case urlCreationError(url: String)
case cantConvertToUUID(id: String) case cantConvertToUUID(id: String)
case missingUserName 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 { public class Services {
@ -49,15 +81,23 @@ public class Services {
// MARK: - Base // MARK: - Base
fileprivate func _runRequest<T : Encodable, U : Decodable>(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U { 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)
}
fileprivate func _runRequest<T: Encodable, U: Decodable>(servicePath: String, method: Method, payload: T, apiCallId: String? = nil) async throws -> U {
var request = try self._baseRequest(servicePath: servicePath, method: method) var request = try self._baseRequest(servicePath: servicePath, method: method)
request.httpBody = try jsonEncoder.encode(payload) request.httpBody = try jsonEncoder.encode(payload)
return try await _runRequest(request, apiCallId: apiCallId) return try await _runRequest(request, apiCallId: apiCallId)
} }
fileprivate func _runRequest<T : Decodable>(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { fileprivate func _runRequest<T: Decodable>(_ request: URLRequest, apiCallId: String? = nil) async throws -> T {
Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) 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 { if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode let statusCode = response.statusCode
Logger.log("request ended with status code = \(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) { if let apiCallId, let type = (T.self as? any Storable.Type) {
try Store.main.rescheduleApiCall(id: apiCallId, type: 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) return try jsonDecoder.decode(T.self, from: task.0)
} }
@ -82,7 +122,11 @@ public class Services {
return try self._baseRequest(servicePath: servicePath, method: .get) 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 let urlString = baseURL + servicePath
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
throw ServiceError.urlCreationError(url: urlString) throw ServiceError.urlCreationError(url: urlString)
@ -90,7 +134,8 @@ public class Services {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = method.rawValue request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
} }
@ -99,17 +144,17 @@ public class Services {
// MARK: - Services // MARK: - Services
func get<T : Storable>() async throws -> [T] { func get<T: Storable>() async throws -> [T] {
let getRequest = try getRequest(servicePath: T.resourceName() + "/") let getRequest = try getRequest(servicePath: T.resourceName() + "/")
return try await self._runRequest(getRequest) return try await self._runRequest(getRequest)
} }
func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T { func runApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
let request = try self._request(from: apiCall) let request = try self._request(from: apiCall)
return try await self._runRequest(request, apiCallId: apiCall.id) return try await self._runRequest(request, apiCallId: apiCall.id)
} }
fileprivate func _request<T : Storable>(from apiCall: ApiCall<T>) throws -> URLRequest { fileprivate func _request<T: Storable>(from apiCall: ApiCall<T>) throws -> URLRequest {
guard let url = URL(string: apiCall.url) else { guard let url = URL(string: apiCall.url) else {
throw ServiceError.urlCreationError(url: apiCall.url) throw ServiceError.urlCreationError(url: apiCall.url)
} }
@ -130,9 +175,9 @@ public class Services {
// MARK: - Authentication // MARK: - Authentication
public func createAccount<U : UserPasswordBase, V : UserBase>(user: U) async throws -> V { public func createAccount<U: UserPasswordBase, V: UserBase>(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) // var postRequest = try self._baseRequest(servicePath: "users/", method: .post)
// postRequest.httpBody = try jsonEncoder.encode(user) // postRequest.httpBody = try jsonEncoder.encode(user)
@ -143,7 +188,7 @@ public class Services {
} }
func requestToken(username: String, password: String) async throws -> String { 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) let credentials = Credentials(username: username, password: password)
postRequest.httpBody = try jsonEncoder.encode(credentials) postRequest.httpBody = try jsonEncoder.encode(credentials)
let response: AuthResponse = try await self._runRequest(postRequest) let response: AuthResponse = try await self._runRequest(postRequest)
@ -153,42 +198,37 @@ public class Services {
fileprivate func _storeToken(username: String, token: String) { fileprivate func _storeToken(username: String, token: String) {
do { do {
try self.keychainStore.deleteToken()
try self.keychainStore.add(username: username, token: token) try self.keychainStore.add(username: username, token: token)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} }
public func login<U : UserBase>(username: String, password: String) async throws -> U { public func login<U: UserBase>(username: String, password: String) async throws -> U {
let token: String = try await requestToken(username: username, password: password) _ = try await requestToken(username: username, password: password)
Logger.log("token = \(token)") let postRequest = try self._baseRequest(conf: .getUser)
var postRequest = try self._baseRequest(servicePath: "plus/user-by-token/", method: .post)
postRequest.httpBody = try jsonEncoder.encode(Token(token: token))
let user: U = try await self._runRequest(postRequest) let user: U = try await self._runRequest(postRequest)
Logger.log("user = \(user.username), id = \(user.id)")
Store.main.setUserUUID(uuidString: user.id) Store.main.setUserUUID(uuidString: user.id)
Store.main.setUserName(user.username)
return user 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 { guard let username = Store.main.userName() else {
throw ServiceError.missingUserName throw ServiceError.missingUserName
} }
struct ChangePasswordParams: Codable { struct ChangePasswordParams: Codable {
var old_password: String
var new_password1: String var new_password1: String
var new_password2: String var new_password2: String
} }
let params = ChangePasswordParams(new_password1: password1, new_password2: password2) let params = ChangePasswordParams(old_password: oldPassword, new_password1: password1, new_password2: password2)
let response: Token = try await self._runRequest( let response: Token = try await self._runRequest(serviceConf: .changePassword, payload: params)
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)
self._storeToken(username: username, token: response.token) self._storeToken(username: username, token: response.token)
} }
@ -220,7 +260,7 @@ struct Token: Codable {
var token: String var token: String
} }
public protocol UserBase : Codable { public protocol UserBase: Codable {
var id: String { get } var id: String { get }
var username: String { get } var username: String { get }
// var password: String? { get } // var password: String? { get }

@ -7,7 +7,7 @@
import Foundation import Foundation
public protocol Storable : Codable, Identifiable where ID : StringProtocol { public protocol Storable: Codable, Identifiable where ID : StringProtocol {
static func resourceName() -> String static func resourceName() -> String
func deleteDependencies() throws func deleteDependencies() throws
} }

@ -80,11 +80,6 @@ public class Store {
return collection return collection
} }
func setupServer<T : UserBase>(url: String, userModel: T) {
self.synchronizationApiURL = url
}
// MARK: - Settings // MARK: - Settings
func setUserUUID(uuidString: String) { func setUserUUID(uuidString: String) {
@ -131,7 +126,7 @@ public class Store {
// MARK: - Convenience // MARK: - Convenience
/// Looks for an instance by id /// Looks for an instance by id
public func findById<T : Storable>(_ id: String) -> T? { public func findById<T: Storable>(_ id: String) -> T? {
guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else { guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered") Logger.w("Collection \(T.resourceName()) not registered")
return nil return nil
@ -140,7 +135,7 @@ public class Store {
} }
/// Filters a collection with a [isIncluded] predicate /// Filters a collection with a [isIncluded] predicate
public func filter<T : Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { public func filter<T: Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
do { do {
return try self.collection().filter(isIncluded) return try self.collection().filter(isIncluded)
} catch { } catch {
@ -149,7 +144,7 @@ public class Store {
} }
/// Returns a collection by type /// Returns a collection by type
func collection<T : Storable>() throws -> StoredCollection<T> { func collection<T: Storable>() throws -> StoredCollection<T> {
if let collection = self._collections[T.resourceName()] as? StoredCollection<T> { if let collection = self._collections[T.resourceName()] as? StoredCollection<T> {
return collection return collection
} }
@ -157,7 +152,7 @@ public class Store {
} }
/// Deletes the dependencies of a collection /// Deletes the dependencies of a collection
public func deleteDependencies<T : Storable>(items: any Sequence<T>) throws { public func deleteDependencies<T: Storable>(items: any Sequence<T>) throws {
try self.collection().deleteDependencies(items) try self.collection().deleteDependencies(items)
} }
@ -169,7 +164,7 @@ public class Store {
} }
/// [beta] Performs the migration if necessary /// [beta] Performs the migration if necessary
func performMigrationIfNecessary<T : Storable>(_ collection: StoredCollection<T>) async throws { func performMigrationIfNecessary<T: Storable>(_ collection: StoredCollection<T>) async throws {
// Check for migrations // Check for migrations
let migrations = self._migrations.filter { $0.resourceName == T.resourceName() } let migrations = self._migrations.filter { $0.resourceName == T.resourceName() }
@ -213,13 +208,13 @@ public class Store {
} }
/// Reschedule an ApiCall by id /// Reschedule an ApiCall by id
func rescheduleApiCall<T : Storable>(id: String, type: T.Type) throws { func rescheduleApiCall<T: Storable>(id: String, type: T.Type) throws {
let collection: StoredCollection<T> = try self.collection() let collection: StoredCollection<T> = try self.collection()
collection.rescheduleApiCallsIfNecessary() collection.rescheduleApiCallsIfNecessary()
} }
/// Executes an ApiCall /// Executes an ApiCall
fileprivate func _executeApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T { fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
guard let service else { guard let service else {
throw StoreError.missingService throw StoreError.missingService
} }
@ -236,7 +231,7 @@ public class Store {
} }
/// Retrieves all the items on the server /// Retrieves all the items on the server
func getItems<T : Storable>() async throws -> [T] { func getItems<T: Storable>() async throws -> [T] {
guard let service else { guard let service else {
throw StoreError.missingService throw StoreError.missingService
} }

@ -7,13 +7,13 @@
import Foundation import Foundation
enum StoredCollectionError : Error { enum StoredCollectionError: Error {
case unmanagedHTTPMethod(method: String) case unmanagedHTTPMethod(method: String)
case missingApiCallCollection case missingApiCallCollection
case missingInstance case missingInstance
} }
protocol SomeCollection : Identifiable { protocol SomeCollection: Identifiable {
func allItems() -> [any Storable] func allItems() -> [any Storable]
func deleteById(_ id: String) throws func deleteById(_ id: String) throws
func deleteApiCallById(_ 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 static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange")
} }
public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollection { public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection {
/// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL /// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL
let synchronized: Bool let synchronized: Bool
@ -82,7 +82,7 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Migrates if necessary and asynchronously decodes the json file /// Migrates if necessary and asynchronously decodes the json file
fileprivate func _load() { fileprivate func _load() {
do { do {
let url = try FileUtils.directoryURLForFileName(T.fileName()) let url = try FileUtils.documentDirectoryURLForFileName(T.fileName())
if FileManager.default.fileExists(atPath: url.path()) { if FileManager.default.fileExists(atPath: url.path()) {
if self.asynchronousIO { if self.asynchronousIO {

@ -19,7 +19,7 @@ class FileUtils {
} }
static func readDocumentFile(fileName: String) throws -> String { 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)") // Logger.log("url = \(fileURL.absoluteString)")
return try String(contentsOf: fileURL, encoding: .utf8) return try String(contentsOf: fileURL, encoding: .utf8)
@ -35,7 +35,7 @@ class FileUtils {
return try String(contentsOf: fileURL, encoding: .utf8) 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 { if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
return dir.appendingPathComponent(fileName) return dir.appendingPathComponent(fileName)
} }

Loading…
Cancel
Save