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
protocol SomeCall : Storable {
protocol SomeCall: Storable {
// func execute() throws
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() }

@ -7,19 +7,19 @@
import Foundation
protocol MicroStorable : Codable {
public protocol MicroStorable : Codable {
init()
static var fileName: String { get }
}
class MicroStorage<T : MicroStorable> {
public class MicroStorage<T : MicroStorable> {
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<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 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<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)
request.httpBody = try jsonEncoder.encode(payload)
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 ?? "")")
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<T : Storable>() async throws -> [T] {
func get<T: Storable>() async throws -> [T] {
let getRequest = try getRequest(servicePath: T.resourceName() + "/")
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)
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 {
throw ServiceError.urlCreationError(url: apiCall.url)
}
@ -130,9 +175,9 @@ public class Services {
// 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)
// 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<U : UserBase>(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<U: UserBase>(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 }

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

@ -80,11 +80,6 @@ public class Store {
return collection
}
func setupServer<T : UserBase>(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<T : Storable>(_ id: String) -> T? {
public func findById<T: Storable>(_ id: String) -> T? {
guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> 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<T : Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
public func filter<T: Storable>(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<T : Storable>() throws -> StoredCollection<T> {
func collection<T: Storable>() throws -> StoredCollection<T> {
if let collection = self._collections[T.resourceName()] as? StoredCollection<T> {
return collection
}
@ -157,7 +152,7 @@ public class Store {
}
/// 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)
}
@ -169,7 +164,7 @@ public class Store {
}
/// [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
let migrations = self._migrations.filter { $0.resourceName == T.resourceName() }
@ -213,13 +208,13 @@ public class Store {
}
/// 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()
collection.rescheduleApiCallsIfNecessary()
}
/// 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 {
throw StoreError.missingService
}
@ -236,7 +231,7 @@ public class Store {
}
/// 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 {
throw StoreError.missingService
}

@ -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<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
let synchronized: Bool
@ -82,7 +82,7 @@ public class StoredCollection<T : Storable> : 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 {

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

Loading…
Cancel
Save