From f926a1fcbe20bdcfbb965c163adb1901522a3e41 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 25 Oct 2024 15:38:24 +0200 Subject: [PATCH] fix issue where updated items needs to copy properties instead of the instance being replaced --- LeStorage.xcodeproj/project.pbxproj | 8 + LeStorage/ApiCallCollection.swift | 8 +- LeStorage/Codables/ApiCall.swift | 12 +- LeStorage/Codables/DataLog.swift | 4 + LeStorage/Codables/FailedAPICall.swift | 12 ++ LeStorage/Codables/GetSyncData.swift | 25 +++ LeStorage/Codables/Log.swift | 7 + LeStorage/Codables/SyncResponse.swift | 126 +++++++++++ LeStorage/Services.swift | 88 ++++---- LeStorage/Storable.swift | 7 +- LeStorage/Store.swift | 6 +- LeStorage/StoreCenter.swift | 275 ++++++++++++++----------- LeStorage/StoredCollection.swift | 202 +++++++++--------- LeStorage/Utils/Errors.swift | 2 +- 14 files changed, 509 insertions(+), 273 deletions(-) create mode 100644 LeStorage/Codables/GetSyncData.swift create mode 100644 LeStorage/Codables/SyncResponse.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 4068fe4..0f43c63 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400D7222CC2AF560092237C /* GetSyncData.swift */; }; + C400D7252CC2B5CF0092237C /* SyncResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400D7242CC2B5CF0092237C /* SyncResponse.swift */; }; C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */ = {isa = PBXBuildFile; fileRef = C425D4382B6D24E1002A7B48 /* LeStorage.docc */; }; C425D4452B6D24E1002A7B48 /* LeStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C425D4372B6D24E1002A7B48 /* LeStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; }; @@ -49,6 +51,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + C400D7222CC2AF560092237C /* GetSyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSyncData.swift; sourceTree = ""; }; + C400D7242CC2B5CF0092237C /* SyncResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResponse.swift; sourceTree = ""; }; C425D4342B6D24E1002A7B48 /* LeStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LeStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C425D4372B6D24E1002A7B48 /* LeStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LeStorage.h; sourceTree = ""; }; C425D4382B6D24E1002A7B48 /* LeStorage.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = LeStorage.docc; sourceTree = ""; }; @@ -177,6 +181,8 @@ C45D35902C0A1DB5000F379F /* FailedAPICall.swift */, C4FC2E302C353E7B0021F3BF /* Log.swift */, C4A47D9A2B7CFFC500ADC637 /* Settings.swift */, + C400D7222CC2AF560092237C /* GetSyncData.swift */, + C400D7242CC2B5CF0092237C /* SyncResponse.swift */, ); path = Codables; sourceTree = ""; @@ -305,11 +311,13 @@ C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */, C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */, C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */, + C400D7252CC2B5CF0092237C /* SyncResponse.swift in Sources */, C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */, C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */, + C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */, C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */, C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */, diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index c21b78c..f025430 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -190,7 +190,13 @@ actor ApiCallCollection: SomeCallCollection { case .delete: let _: Empty = try await self._executeApiCall(apiCall) case .get: - let _: [T] = try await self._executeApiCall(apiCall) + if T.self == GetSyncData.self { + let _: Empty = try await self._executeApiCall(apiCall) + } else { + let _: [T] = try await self._executeApiCall(apiCall) + } + // process GET + // what if it is a sync GET } } catch { // Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index 9eb7752..f48aee1 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -27,11 +27,11 @@ class ApiCall: ModelObject, Storable, SomeCall { /// The HTTP method of the call: post... var method: HTTPMethod - /// The id of the underlying data - var dataId: String - /// The content of the call - var body: String + var body: String? + + /// The id of the underlying data stored in the body + var dataId: String? /// The number of times the call has been executed var attemptsCount: Int = 0 @@ -45,4 +45,8 @@ class ApiCall: ModelObject, Storable, SomeCall { self.body = body } + func copy(from other: any Storable) { + fatalError("should not happen") + } + } diff --git a/LeStorage/Codables/DataLog.swift b/LeStorage/Codables/DataLog.swift index 19424fe..c4797d8 100644 --- a/LeStorage/Codables/DataLog.swift +++ b/LeStorage/Codables/DataLog.swift @@ -30,4 +30,8 @@ class DataLog: ModelObject, Storable { self.operation = operation } + func copy(from other: any Storable) { + fatalError("should not happen") + } + } diff --git a/LeStorage/Codables/FailedAPICall.swift b/LeStorage/Codables/FailedAPICall.swift index 737403c..b5fdd5d 100644 --- a/LeStorage/Codables/FailedAPICall.swift +++ b/LeStorage/Codables/FailedAPICall.swift @@ -41,4 +41,16 @@ class FailedAPICall: SyncedModelObject, SyncedStorable { self.authentication = authentication } + func copy(from other: any Storable) { + + guard let fac = other as? FailedAPICall else { return } + + self.date = fac.date + self.callId = fac.callId + self.type = fac.type + self.apiCall = fac.apiCall + self.error = fac.error + self.authentication = fac.authentication + } + } diff --git a/LeStorage/Codables/GetSyncData.swift b/LeStorage/Codables/GetSyncData.swift new file mode 100644 index 0000000..d12a403 --- /dev/null +++ b/LeStorage/Codables/GetSyncData.swift @@ -0,0 +1,25 @@ +// +// SyncData.swift +// LeStorage +// +// Created by Laurent Morvillier on 18/10/2024. +// + +import Foundation + +class GetSyncData: ModelObject, SyncedStorable { + + static func filterByStoreIdentifier() -> Bool { return false } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + var lastUpdate: Date = Date() + + static func resourceName() -> String { + return "data" + } + + func copy(from other: any Storable) { + guard let getSyncData = other as? GetSyncData else { return } + self.lastUpdate = getSyncData.lastUpdate + } + +} diff --git a/LeStorage/Codables/Log.swift b/LeStorage/Codables/Log.swift index e767c86..c8f7ebd 100644 --- a/LeStorage/Codables/Log.swift +++ b/LeStorage/Codables/Log.swift @@ -23,4 +23,11 @@ class Log: SyncedModelObject, SyncedStorable { self.message = message } + func copy(from other: any Storable) { + guard let log = other as? Log else { return } + + self.date = log.date + self.message = log.message + } + } diff --git a/LeStorage/Codables/SyncResponse.swift b/LeStorage/Codables/SyncResponse.swift new file mode 100644 index 0000000..55f2d06 --- /dev/null +++ b/LeStorage/Codables/SyncResponse.swift @@ -0,0 +1,126 @@ +// +// SyncResponse.swift +// LeStorage +// +// Created by Laurent Morvillier on 18/10/2024. +// + +import Foundation + +struct SyncResponse: Codable { + let updates: [String: [Codable]] + let deletions: [String: [Int]] + let date: String? + + enum CodingKeys: String, CodingKey { + case updates, deletions, date + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + deletions = try container.decode([String: [Int]].self, forKey: .deletions) + date = try container.decodeIfPresent(String.self, forKey: .date) + + let updatesContainer = try container.nestedContainer( + keyedBy: DynamicCodingKeys.self, forKey: .updates) + var updatesDict = [String: [AnyCodable]]() + + for key in updatesContainer.allKeys { + let swiftClass = try SyncResponse._classFromClassName(key.stringValue) + let decodedArray = try updatesContainer.decode([AnyCodable].self, forKey: key) + let typedArray = decodedArray.compactMap { $0.value as? AnyCodable } + updatesDict[key.stringValue] = typedArray + } + updates = updatesDict + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(deletions, forKey: .deletions) + try container.encodeIfPresent(date, forKey: .date) + + var updatesContainer = container.nestedContainer( + keyedBy: DynamicCodingKeys.self, forKey: .updates) + for (key, value) in updates { + let encodableArray = value.map { AnyCodable($0) } + try updatesContainer.encode( + encodableArray, forKey: DynamicCodingKeys(stringValue: key)!) + } + } + + struct DynamicCodingKeys: CodingKey { + var stringValue: String + init?(stringValue: String) { + self.stringValue = stringValue + } + var intValue: Int? + init?(intValue: Int) { + return nil + } + } + + fileprivate static func _classFromClassName(_ className: String) throws -> Codable.Type { + + let fullClassName = "PadelClub.\(className)" + let modelClass: AnyClass? = NSClassFromString(fullClassName) + if let type = modelClass as? Codable.Type { + return type + } else { + throw LeStorageError.cantFindClassFromName(name: className) + } + + } + +} + +struct AnyCodable: Codable { + let value: Any + + init(_ value: Any) { + self.value = value + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let intValue = try? container.decode(Int.self) { + value = intValue + } else if let doubleValue = try? container.decode(Double.self) { + value = doubleValue + } else if let boolValue = try? container.decode(Bool.self) { + value = boolValue + } else if let stringValue = try? container.decode(String.self) { + value = stringValue + } else if let arrayValue = try? container.decode([AnyCodable].self) { + value = arrayValue.map { $0.value } + } else if let dictionaryValue = try? container.decode([String: AnyCodable].self) { + value = dictionaryValue.mapValues { $0.value } + } else { + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "AnyCodable value cannot be decoded") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch value { + case let intValue as Int: + try container.encode(intValue) + case let doubleValue as Double: + try container.encode(doubleValue) + case let boolValue as Bool: + try container.encode(boolValue) + case let stringValue as String: + try container.encode(stringValue) + case let arrayValue as [Any]: + try container.encode(arrayValue.map { AnyCodable($0) }) + case let dictionaryValue as [String: Any]: + try container.encode(dictionaryValue.mapValues { AnyCodable($0) }) + default: + throw EncodingError.invalidValue( + value, + EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "AnyCodable value cannot be encoded")) + } + } +} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 688fe3f..879c5ff 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -87,16 +87,21 @@ public class Services { _ request: URLRequest, apiCall: ApiCall ) async throws -> V { let debugURL = request.url?.absoluteString ?? "" - print("Run \(request.httpMethod ?? "") \(debugURL)") +// print("Run \(request.httpMethod ?? "") \(debugURL)") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) - print("response = \(String(data: task.0, encoding: .utf8) ?? "")") + print("\(apiCall.method.rawValue) \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")") if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode print("\(debugURL) ended, status code = \(statusCode)") switch statusCode { - case 200..<300: // success + case 200..<300: // success try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id) + + if T.self == GetSyncData.self { + StoreCenter.main.synchronizeContent(task.0, decoder: self.jsonDecoder) + } + default: // error Logger.log( "Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") @@ -120,22 +125,27 @@ public class Services { Logger.w(message) } - if !(V.self is Empty?.Type) { - return try jsonDecoder.decode(V.self, from: task.0) + return try self._decode(data: task.0) + + } + + fileprivate func _decode(data: Data) throws -> V { + if !(V.self is Empty?.Type || V.self is Empty.Type) { + return try jsonDecoder.decode(V.self, from: data) } else { return try jsonDecoder.decode(V.self, from: "{}".data(using: .utf8)!) } } - + /// 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) async throws -> V { let debugURL = request.url?.absoluteString ?? "" - print("Run \(request.httpMethod ?? "") \(debugURL)") +// print("Run \(request.httpMethod ?? "") \(debugURL)") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) - print("response = \(String(data: task.0, encoding: .utf8) ?? "")") + print("\(request.httpMethod ?? "") \(debugURL) => \(String(data: task.0, encoding: .utf8) ?? "")") if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode @@ -158,7 +168,7 @@ public class Services { StoreCenter.main.log(message: message) Logger.w(message) } - return try jsonDecoder.decode(V.self, from: task.0) + return try self._decode(data: task.0) } /// Returns if the token is required for a request @@ -190,31 +200,31 @@ public class Services { /// 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) - } +// 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: @@ -286,7 +296,7 @@ public class Services { guard let url = URL(string: urlString) else { throw ServiceError.urlCreationError(url: urlString) } - guard let bodyData = apiCall.body.data(using: .utf8) else { + guard let body = apiCall.body, let bodyData = body.data(using: .utf8) else { throw ServiceError.cantDecodeData(content: apiCall.body) } @@ -352,9 +362,9 @@ public class Services { /// - request: The synchronization request fileprivate func _runGetSyncLogRequest(_ request: URLRequest) async throws { let debugURL = request.url?.absoluteString ?? "" - print("Run \(request.httpMethod ?? "") \(debugURL)") +// print("Run \(request.httpMethod ?? "") \(debugURL)") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) - print("response = \(String(data: task.0, encoding: .utf8) ?? "")") + print("\(request.httpMethod ?? "") \(debugURL) => \(String(data: task.0, encoding: .utf8) ?? "")") if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode @@ -422,7 +432,7 @@ public class Services { /// Executes an ApiCall func runApiCall(_ apiCall: ApiCall) async throws -> V { let request = try self._syncRequest(from: apiCall) - print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") +// print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") return try await self._runRequest(request, apiCall: apiCall) } @@ -433,7 +443,7 @@ public class Services { let url = try self._url(from: apiCall) var request = URLRequest(url: url) request.httpMethod = apiCall.method.rawValue - request.httpBody = apiCall.body.data(using: .utf8) + request.httpBody = apiCall.body?.data(using: .utf8) request.setValue("application/json", forHTTPHeaderField: "Content-Type") if self._isTokenRequired(type: T.self, method: apiCall.method) { diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 2fb1d83..73da501 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -9,7 +9,7 @@ import Foundation /// A protocol describing classes that can be stored locally in JSON and synchronized on our django server public protocol Storable: Codable, Identifiable, NSObjectProtocol { - + /// The store containing a reference to the instance var store: Store? { get set } @@ -28,10 +28,13 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol { /// so when we do that on the server, we also need to do it locally func deleteDependencies() - static var relationshipNames: [String] { get } +// static var relationshipNames: [String] { get } /// A method called after the instance has been deleted from its StoredCollection func hasBeenDeleted() + + func copy(from other: any Storable) + } extension Storable { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 8457a73..c9025c3 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -147,7 +147,7 @@ open class Store { /// Loads all collection with the data from the server public func loadCollectionsFromServer() { - for collection in self._StoredCollections() { + for collection in self._syncedCollections() { Task { try? await collection.loadDataFromServerIfAllowed() } @@ -156,7 +156,7 @@ open class Store { /// Loads all synchronized collection with server data if they don't already have a local file public func loadCollectionsFromServerIfNoFile() { - for collection in self._StoredCollections() { + for collection in self._syncedCollections() { Task { do { try await collection.loadCollectionsFromServerIfNoFile() @@ -167,7 +167,7 @@ open class Store { } } - fileprivate func _StoredCollections() -> [any SomeSyncedCollection] { + fileprivate func _syncedCollections() -> [any SomeSyncedCollection] { return self._collections.values.compactMap { $0 as? any SomeSyncedCollection } } diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 3577d51..fd8babf 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -9,13 +9,13 @@ import Foundation import UIKit public class StoreCenter { - + /// The main instance public static let main: StoreCenter = StoreCenter() - + /// A dictionary of Stores associated to their id - fileprivate var _stores: [String : Store] = [:] - + fileprivate var _stores: [String: Store] = [:] + /// The URL of the django API public var synchronizationApiURL: String? { didSet { @@ -24,39 +24,40 @@ public class StoreCenter { } } } - + /// Indicates to Stored Collection if they can synchronize public var collectionsCanSynchronize: Bool = true - + /// Force the absence of synchronization public var forceNoSynchronization: Bool = false - + /// A store for the Settings object - fileprivate var _settingsStorage: MicroStorage = MicroStorage(fileName: "settings.json") - + fileprivate var _settingsStorage: MicroStorage = MicroStorage( + fileName: "settings.json") + /// The services performing the API calls fileprivate var _services: Services? - + /// The dictionary of registered StoredCollections - fileprivate var _apiCallCollections: [String : any SomeCallCollection] = [:] - + fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] + /// A collection of DataLog objects, used for the synchronization fileprivate var _dataLogs: StoredCollection - + /// A collection storing FailedAPICall objects fileprivate var _failedAPICallsCollection: StoredCollection? = nil - + /// A collection of Log objects fileprivate var _logs: StoredCollection? = nil - + /// A list of username that cannot synchronize with the server fileprivate var _blackListedUserName: [String] = [] - + init() { self._dataLogs = Store.main.registerCollection() self._setupNotifications() } - + /// Returns the service instance public func service() throws -> Services { if let service = self._services { @@ -65,7 +66,7 @@ public class StoreCenter { throw StoreError.missingService } } - + private func _setupNotifications() { NotificationCenter.default.addObserver( self, @@ -78,9 +79,9 @@ public class StoreCenter { Logger.log("_willEnterForegroundNotification") self._launchSynchronization() } - + @objc fileprivate func _launchSynchronization() { - Task{ + Task { do { try await self.synchronizeLastUpdates() } catch { @@ -101,7 +102,7 @@ public class StoreCenter { } self._stores[identifier] = store } - + /// Returns a store using its identifier, and registers it if it does not exists /// - Parameters: /// - identifier: The store identifer @@ -115,9 +116,9 @@ public class StoreCenter { return store } } - + // MARK: - Settings - + /// Sets the user info given a user func setUserInfo(user: UserBase) { self._settingsStorage.update { settings in @@ -125,37 +126,37 @@ public class StoreCenter { settings.username = user.username } } - + /// Returns the stored user Id public var userId: String? { return self._settingsStorage.item.userId } - + /// Returns the username public func userName() -> String? { return self._settingsStorage.item.username } - + /// Returns the stored token public func token() -> String? { return try? self.service().keychainStore.getValue() } - + /// Disconnect the user from the storage and resets collection public func disconnect() { try? self.service().deleteToken() - + self.resetApiCalls() self._failedAPICallsCollection?.reset() - + self._settingsStorage.update { settings in settings.username = nil settings.userId = nil settings.lastSynchronization = nil } - + } - + /// Returns whether the system has a user token public func hasToken() -> Bool { do { @@ -165,7 +166,7 @@ public class StoreCenter { return false } } - + /// Returns a generated device id /// If created, stores it inside the keychain to get a consistent value even if the app is deleted /// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again @@ -174,8 +175,8 @@ public class StoreCenter { do { return try keychainStore.getValue() } catch { - let deviceId: String = UIDevice.current.identifierForVendor?.uuidString ?? - UUID().uuidString + let deviceId: String = + UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString do { try keychainStore.add(value: deviceId) } catch { @@ -184,9 +185,9 @@ public class StoreCenter { return deviceId } } - + // MARK: - Api Calls management - + /// Instantiates and loads an ApiCallCollection with the provided type public func loadApiCallCollection(type: T.Type) { if self._apiCallCollections[T.resourceName()] == nil { @@ -201,7 +202,7 @@ public class StoreCenter { } } } - + /// Returns the ApiCall collection using the resource name of the provided T type func apiCallCollection() throws -> ApiCallCollection { if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection { @@ -218,7 +219,7 @@ public class StoreCenter { let apiCallCollection: ApiCallCollection = try self.apiCallCollection() await apiCallCollection.deleteByDataId(id) } - + /// Deletes an ApiCall by its id /// - Parameters: /// - type: the subsequent type of the ApiCall @@ -227,7 +228,7 @@ public class StoreCenter { let apiCallCollection: ApiCallCollection = try self.apiCallCollection() await apiCallCollection.deleteById(id) } - + /// Deletes an ApiCall by its id /// - Parameters: /// - id: the id of the ApiCall @@ -239,7 +240,7 @@ public class StoreCenter { throw StoreError.collectionNotRegistered(type: collectionName) } } - + /// Resets all the api call collections public func resetApiCalls() { Task { @@ -248,7 +249,7 @@ public class StoreCenter { } } } - + /// Resets the ApiCall whose type identifies with the provided collection /// - Parameters: /// - collection: The collection identifying the Storable type @@ -262,9 +263,9 @@ public class StoreCenter { Logger.error(error) } } - + // MARK: - Api call rescheduling - + /// Reschedule an ApiCall by id func rescheduleApiCalls(id: String, type: T.Type) async throws { guard self.collectionsCanSynchronize else { @@ -273,24 +274,27 @@ public class StoreCenter { let collection: ApiCallCollection = try self.apiCallCollection() await collection.rescheduleApiCallsIfNecessary() } - + /// Executes an ApiCall - fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> V { + fileprivate func _executeApiCall(_ apiCall: ApiCall) + async throws -> V + { return try await self.service().runApiCall(apiCall) } - + /// Executes an API call func execute(apiCall: ApiCall) async throws -> V { return try await self._executeApiCall(apiCall) } - + // MARK: - Api calls - + /// Returns whether the collection can synchronize fileprivate func _canSynchronise() -> Bool { - return !self.forceNoSynchronization && self.collectionsCanSynchronize && self.userIsAllowed() + return !self.forceNoSynchronization && self.collectionsCanSynchronize + && self.userIsAllowed() } - + /// Transmit the insertion request to the ApiCall collection /// - Parameters: /// - instance: an object to insert @@ -300,7 +304,7 @@ public class StoreCenter { } return try await self.apiCallCollection().sendInsertion(instance) } - + /// Transmit the update request to the ApiCall collection /// - Parameters: /// - instance: an object to update @@ -310,7 +314,7 @@ public class StoreCenter { } return try await self.apiCallCollection().sendUpdate(instance) } - + /// Transmit the deletion request to the ApiCall collection /// - Parameters: /// - instance: an object to delete @@ -320,13 +324,17 @@ public class StoreCenter { } try await self.apiCallCollection().sendDeletion(instance) } - + /// Retrieves all the items on the server func getItems(identifier: StoreIdentifier? = nil) async throws -> [T] { return try await self.service().get(identifier: identifier) } - + // MARK: - Synchronization + + fileprivate func _createSyncApiCallCollection() { + self.loadApiCallCollection(type: GetSyncData.self) + } public func initialSynchronization() { self._settingsStorage.update { settings in @@ -334,16 +342,17 @@ public class StoreCenter { } Store.main.loadCollectionsFromServer() } - + public func synchronizeLastUpdates() async throws { let lastSync: Date? = self._settingsStorage.item.lastSynchronization try await self._services?.synchronizeLastUpdates(since: lastSync) } - + func synchronizeContent(_ data: Data, decoder: JSONDecoder) { - + do { - guard let json = try JSONSerialization.jsonObject(with: data, options: []) + guard + let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { Logger.w("data unrecognized") @@ -370,32 +379,32 @@ public class StoreCenter { if let dateString: String = json["date"] as? String, let syncDate = Date.iso8601Formatter.date(from: dateString) { - self._settingsStorage.update { settings in settings.lastSynchronization = syncDate } } - + } catch { Logger.error(error) } } - + fileprivate func _parseSyncUpdates(_ updates: [String: Any], decoder: JSONDecoder) throws { for (className, updateData) in updates { guard let updateArray = updateData as? [[String: Any]] else { Logger.w("Invalid update data for \(className)") continue } - + let type = try self._classFromClassName(className) for updateItem in updateArray { - + do { - let jsonData = try JSONSerialization.data(withJSONObject: updateItem, options: []) + let jsonData = try JSONSerialization.data( + withJSONObject: updateItem, options: []) let decodedObject = try decoder.decode(type, from: jsonData) - + let storeId: String? = decodedObject.getStoreId() StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId) } catch { @@ -404,25 +413,27 @@ public class StoreCenter { } } } - + fileprivate func _parseSyncDeletions(_ deletions: [String: Any], decoder: JSONDecoder) throws { for (className, updateDeletions) in deletions { guard let deletionArray = updateDeletions as? [[String: Any]] else { Logger.w("Invalid update data for \(className)") continue } - + let type = try self._classFromClassName(className) - + for updateItem in deletionArray { - + if let object = updateItem["data"] { do { - let jsonData = try JSONSerialization.data(withJSONObject: object, options: []) + let jsonData = try JSONSerialization.data( + withJSONObject: object, options: []) let decodedObject = try decoder.decode(type, from: jsonData) - + let storeId = updateItem["storeId"] as? String - StoreCenter.main.synchronizationDelete(instance: decodedObject, storeId: storeId) + StoreCenter.main.synchronizationDelete( + instance: decodedObject, storeId: storeId) } catch { Logger.error(error) } @@ -430,9 +441,9 @@ public class StoreCenter { } } } - + fileprivate func _classFromClassName(_ className: String) throws -> any SyncedStorable.Type { - + let fullClassName = "PadelClub.\(className)" let modelClass: AnyClass? = NSClassFromString(fullClassName) if let type = modelClass as? any SyncedStorable.Type { @@ -440,9 +451,9 @@ public class StoreCenter { } else { throw LeStorageError.cantFindClassFromName(name: className) } - + } - + fileprivate func _store(id: String?) -> Store? { if let storeId = id { return self._stores[storeId] @@ -450,47 +461,50 @@ public class StoreCenter { return Store.main } } - + fileprivate func _hasAlreadyBeenDeleted(_ instance: T) -> Bool { - return self._dataLogs.contains(where: { $0.dataId == instance.stringId && $0.operation == .delete }) + return self._dataLogs.contains(where: { + $0.dataId == instance.stringId && $0.operation == .delete + }) } - + func synchronizationAddOrUpdate(_ instance: T, storeId: String?) { let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) if !hasAlreadyBeenDeleted { - DispatchQueue.main.async { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self._store(id: storeId)?.addOrUpdateIfNewer(instance) } } } - + func synchronizationDelete(instance: T, storeId: String?) { DispatchQueue.main.async { self._store(id: storeId)?.deleteNoSync(instance: instance) self._cleanupDataLog(dataId: instance.stringId) } } - + fileprivate func _cleanupDataLog(dataId: String) { let logs = self._dataLogs.filter { $0.dataId == dataId } self._dataLogs.delete(contentOfs: logs) } - -// func createInsertLog(_ instance: T) { -// self._addDataLog(instance, method: .post) -// } + + // func createInsertLog(_ instance: T) { + // self._addDataLog(instance, method: .post) + // } func createDeleteLog(_ instance: T) { self._addDataLog(instance, method: .delete) } - + fileprivate func _addDataLog(_ instance: T, method: HTTPMethod) { - let dataLog = DataLog(dataId: instance.stringId, modelName: String(describing: T.self), operation: method) + let dataLog = DataLog( + dataId: instance.stringId, modelName: String(describing: T.self), operation: method) self._dataLogs.addOrUpdate(instance: dataLog) } - + // MARK: - Miscellanous - + public func apiCallCount(type: T.Type) async -> Int { do { let collection: ApiCallCollection = try self.apiCallCollection() @@ -499,7 +513,7 @@ public class StoreCenter { return -1 } } - + /// Resets all registered collection public func reset() { Store.main.reset() @@ -507,7 +521,7 @@ public class StoreCenter { store.reset() } } - + /// Returns whether any collection has pending API calls public func hasPendingAPICalls() async -> Bool { for collection in self._apiCallCollections.values { @@ -522,33 +536,39 @@ public class StoreCenter { public func apiCallsFileContent(resourceName: String) async -> String { return await self._apiCallCollections[resourceName]?.contentOfFile() ?? "" } - + /// This method triggers the framework to save and send failed api calls public func logsFailedAPICalls() { self._failedAPICallsCollection = Store.main.registerCollection() } - + /// 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, request: URLRequest, collectionName: String, error: String) { - + func logFailedAPICall( + _ apiCallId: String, request: URLRequest, collectionName: String, error: String + ) { + guard let failedAPICallsCollection = self._failedAPICallsCollection, - let collection = self._apiCallCollections[collectionName], - collectionName != FailedAPICall.resourceName() - else { + let collection = self._apiCallCollections[collectionName], + collectionName != FailedAPICall.resourceName() + else { return } - + Task { if let apiCall = await collection.findCallById(apiCallId) { - - if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 { + + if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) + && apiCall.attemptsCount > 6 + { do { let authValue = request.allHTTPHeaderFields?["Authorization"] let string = try apiCall.jsonString() - let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error, authentication: authValue) - + let failedAPICall = FailedAPICall( + callId: apiCall.id, type: collectionName, apiCall: string, error: error, + authentication: authValue) + DispatchQueue.main.async { failedAPICallsCollection.addOrUpdate(instance: failedAPICall) } @@ -558,25 +578,28 @@ public class StoreCenter { } } } - + } - + /// Logs a failed Api call with its request and error message func logFailedAPICall(request: URLRequest, error: String) { guard let failedAPICallsCollection = self._failedAPICallsCollection, - let body: Data = request.httpBody, - let bodyString = String(data: body, encoding: .utf8), - let url = request.url?.absoluteString else { + let body: Data = request.httpBody, + let bodyString = String(data: body, encoding: .utf8), + let url = request.url?.absoluteString + else { return } - + let authValue = request.allHTTPHeaderFields?["Authorization"] - let failedAPICall = FailedAPICall(callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, authentication: authValue) + let failedAPICall = FailedAPICall( + callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, + authentication: authValue) failedAPICallsCollection.addOrUpdate(instance: failedAPICall) } - + /// Adds a userName to the black list /// Black listed username cannot send data to the server /// - Parameters: @@ -584,15 +607,15 @@ public class StoreCenter { public func blackListUserName(_ userName: String) { self._blackListedUserName.append(userName) } - + /// Returns whether the current userName is allowed to sync with the server func userIsAllowed() -> Bool { guard let userName = self.userName() else { return true } - return !self._blackListedUserName.contains(where: { $0 == userName } ) + return !self._blackListedUserName.contains(where: { $0 == userName }) } - + /// Deletes the directory using its identifier /// - Parameters: /// - identifier: The name of the directory @@ -601,9 +624,9 @@ public class StoreCenter { FileManager.default.deleteDirectoryInDocuments(directoryName: directory) self._stores.removeValue(forKey: identifier) } - + // MARK: - Instant update - + /// Updates a local object with a server instance func updateFromServerInstance(_ result: T) { if let storedCollection: StoredCollection = self.collectionOfInstance(result) { @@ -612,7 +635,7 @@ public class StoreCenter { } } } - + /// Returns the collection hosting an instance func collectionOfInstance(_ instance: T) -> StoredCollection? { do { @@ -626,7 +649,7 @@ public class StoreCenter { return self.collectionOfInstanceInSubStores(instance) } } - + /// Search inside the additional stores to find the collection hosting the instance func collectionOfInstanceInSubStores(_ instance: T) -> StoredCollection? { for store in self._stores.values { @@ -637,9 +660,9 @@ public class StoreCenter { } return nil } - + // MARK: - Logs - + /// Returns the logs collection and instantiates it if necessary fileprivate func _logsCollection() -> StoredCollection { if let logs = self._logs { @@ -650,15 +673,15 @@ public class StoreCenter { return logsCollection } } - + /// Logs a message in the logs collection public func log(message: String) { let log = Log(message: message) self._logsCollection().addOrUpdate(instance: log) } - + // MARK: - Migration - + /// Migrates the token from the provided service to the main Services instance public func migrateToken(_ services: Services) throws { guard let userName = self.userName() else { @@ -666,9 +689,9 @@ public class StoreCenter { } try self.service().migrateToken(services, userName: userName) } - + deinit { NotificationCenter.default.removeObserver(self) } - + } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 52acbc0..b463992 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -15,7 +15,7 @@ enum StoredCollectionError: Error { protocol CollectionHolder { associatedtype Item - + var items: [Item] { get } func reset() } @@ -33,78 +33,82 @@ protocol SomeSyncedCollection: SomeCollection { } extension Notification.Name { - public static let CollectionDidLoad: Notification.Name = Notification.Name.init("notification.collectionDidLoad") - public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange") + public static let CollectionDidLoad: Notification.Name = Notification.Name.init( + "notification.collectionDidLoad") + public static let CollectionDidChange: Notification.Name = Notification.Name.init( + "notification.collectionDidChange") } -public class StoredCollection: RandomAccessCollection, SomeCollection, CollectionHolder { - +public class StoredCollection: RandomAccessCollection, SomeCollection, CollectionHolder +{ + /// Doesn't write the collection in a file fileprivate(set) var inMemory: Bool = false - + /// The list of stored items @Published public fileprivate(set) var items: [T] = [] - + /// The reference to the Store fileprivate(set) var store: Store - + /// Provides fast access for instances if the collection has been instanced with [indexed] = true - fileprivate var _indexes: [T.ID : T]? = nil - + fileprivate var _indexes: [T.ID: T]? = nil + /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _hasChanged: Bool = false { didSet { if self._hasChanged == true { - + self._scheduleWrite() DispatchQueue.main.async { - NotificationCenter.default.post(name: NSNotification.Name.CollectionDidChange, object: self) + NotificationCenter.default.post( + name: NSNotification.Name.CollectionDidChange, object: self) } self._hasChanged = false } } } - + /// Denotes a collection that loads and writes asynchronously fileprivate var asynchronousIO: Bool = true - + /// Indicates if the collection has loaded locally, with or without a file fileprivate(set) public var hasLoaded: Bool = false - + init(store: Store, indexed: Bool = false, asynchronousIO: Bool = true, inMemory: Bool = false) { -// self.synchronized = synchronized + // self.synchronized = synchronized self.asynchronousIO = asynchronousIO if indexed { self._indexes = [:] } self.inMemory = inMemory self.store = store - + self.load() } - + fileprivate init() { -// self.synchronized = false + // self.synchronized = false self.store = Store.main } - + public static func placeholder() -> StoredCollection { return StoredCollection() } - + var resourceName: String { return T.resourceName() } - + // MARK: - Loading - + func setChanged() { self._hasChanged = true } - + /// Migrates if necessary and asynchronously decodes the json file func load() { - + do { if !self.inMemory { try self.loadFromFile() @@ -112,12 +116,12 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } catch { Logger.error(error) } - + } - + /// Starts the JSON file decoding synchronously or asynchronously func loadFromFile() throws { - + if self.asynchronousIO { Task(priority: .high) { try self._decodeJSONFile() @@ -125,14 +129,14 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } else { try self._decodeJSONFile() } - + } - + /// Decodes the json file into the items array fileprivate func _decodeJSONFile() throws { - + let fileURL = try self.store.fileURL(type: T.self) - + if FileManager.default.fileExists(atPath: fileURL.path()) { let jsonString: String = try FileUtils.readFile(fileURL: fileURL) let decoded: [T] = try jsonString.decodeArray() ?? [] @@ -152,51 +156,52 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.setAsLoaded() } } - + /// Sets the collection as loaded /// Send a CollectionDidLoad event func setAsLoaded() { self.hasLoaded = true DispatchQueue.main.async { - NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self) + NotificationCenter.default.post( + name: NSNotification.Name.CollectionDidLoad, object: self) } } - + /// Sets a collection of items and indexes them fileprivate func _setItems(_ items: [T]) { self.items = items self._updateIndexIfNecessary() } - + /// Updates the whole index with the items array fileprivate func _updateIndexIfNecessary() { - if let _ = self._indexes { + if self._indexes != nil { self._indexes = self.items.dictionary { $0.id } } } - + // MARK: - Basic operations - + /// Adds or updates the provided instance inside the collection /// Adds it if its id is not found, and otherwise updates it public func addOrUpdate(instance: T) { self.addOrUpdateItem(instance: instance) } - + func addOrUpdateItem(instance: T) { - + defer { self._hasChanged = true } - + if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.updateItem(instance, index: index) } else { self.addItem(instance: instance) } - + } - + /// A method the treat the collection as a single instance holder func setSingletonNoSync(instance: T) { defer { @@ -205,7 +210,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.items.removeAll() self.addItem(instance: instance) } - + /// Deletes the instance in the collection and sets the collection as changed to trigger a write public func delete(instance: T) throws { defer { @@ -213,51 +218,51 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } self.deleteItem(instance) } - + /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write public func delete(contentOfs sequence: any Sequence) { - + defer { self._hasChanged = true } - + for instance in sequence { self.deleteItem(instance) } } - + /// Deletes an instance in the collection. Also: /// - Removes its reference from the index /// - Notifies the server of the deletion /// - Calls `hasBeenDeleted` on the deleted instance -// fileprivate func _delete(_ instance: T) throws { -// instance.deleteDependencies() -// self.items.removeAll { $0.id == instance.id } -// self._indexes?.removeValue(forKey: instance.id) -// instance.hasBeenDeleted() -// } - + // fileprivate func _delete(_ instance: T) throws { + // instance.deleteDependencies() + // self.items.removeAll { $0.id == instance.id } + // self._indexes?.removeValue(forKey: instance.id) + // instance.hasBeenDeleted() + // } + /// Adds or update a sequence of elements public func addOrUpdate(contentOfs sequence: any Sequence) { self.addSequence(sequence) -// self._addOrUpdate(contentOfs: sequence) + // self._addOrUpdate(contentOfs: sequence) } func addSequence(_ sequence: any Sequence) { defer { self._hasChanged = true } - + for instance in sequence { if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.updateItem(instance, index: index) - } else { // insert + } else { // insert self.addItem(instance: instance) } } } - + fileprivate func _affectStoreIdIfNecessary(instance: T) { if let storeId = self.store.identifier?.value { if var altStorable = instance as? SideStorable { @@ -267,7 +272,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } } } - + func addItem(instance: T) { self._affectStoreIdIfNecessary(instance: instance) self.items.append(instance) @@ -276,18 +281,21 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } func updateItem(_ instance: T, index: Int) { - self.items[index] = instance +// var existingItem = self.items[index] +// existingItem.hasBeenDeleted() +// self._copy(instance, into: &existingItem) // we need to keep the instance alive for screen to refresh + self.items[index].copy(from: instance) instance.store = self.store self._indexes?[instance.id] = instance } - + func deleteItem(_ instance: T) { instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } self._indexes?.removeValue(forKey: instance.id) instance.hasBeenDeleted() } - + /// Returns the instance corresponding to the provided [id] public func findById(_ id: T.ID) -> T? { if let index = self._indexes, let instance = index[id] { @@ -295,98 +303,98 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } return self.items.first(where: { $0.id == id }) } - + /// Deletes the instance corresponding to the provided [id] public func deleteById(_ id: T.ID) throws { if let instance = self.findById(id) { self.deleteItem(instance) -// try self.delete(instance: instance) + // try self.delete(instance: instance) } } - + /// Proceeds to "hard" delete the items without synchronizing them /// Also removes related API calls public func deleteDependencies(_ items: any Sequence) { defer { self._hasChanged = true } - let itemsArray = Array(items) // fix error if items is self.items + let itemsArray = Array(items) // fix error if items is self.items for item in itemsArray { if let index = self.items.firstIndex(where: { $0.id == item.id }) { self.items.remove(at: index) } item.hasBeenDeleted() - -// Task { -// do { -// try await StoreCenter.main.deleteApiCallByDataId(type: T.self, id: item.stringId) -// } catch { -// Logger.error(error) -// } -// } + + // Task { + // do { + // try await StoreCenter.main.deleteApiCallByDataId(type: T.self, id: item.stringId) + // } catch { + // Logger.error(error) + // } + // } } - + } - + /// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls -// public func deleteAll() throws { -// try self.delete(contentOfs: self.items) -// } - + // public func deleteAll() throws { + // try self.delete(contentOfs: self.items) + // } + // MARK: - SomeCall - + /// Returns the collection items as [any Storable] func allItems() -> [any Storable] { return self.items } - + // MARK: - File access - + /// Schedules a write operation fileprivate func _scheduleWrite() { - + guard !self.inMemory else { return } - + if self.asynchronousIO { - DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { // sync to make sure we don't have writes performed at the same time + DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { // sync to make sure we don't have writes performed at the same time self._write() } } else { self._write() } } - + /// Writes all the items as a json array inside a file fileprivate func _write() { -// Logger.log("Start write to \(T.fileName())...") + // Logger.log("Start write to \(T.fileName())...") do { let jsonString: String = try self.items.jsonString() try self.store.write(content: jsonString, fileName: T.fileName()) } catch { - Logger.error(error) // TODO how to notify the main project + Logger.error(error) // TODO how to notify the main project } -// Logger.log("End write") + // Logger.log("End write") } - + /// Simply clears the items of the collection func clear() { self.items.removeAll() } - + /// Removes the items of the collection and deletes the corresponding file public func reset() { self.items.removeAll() self.store.removeFile(type: T.self) } - + // MARK: - RandomAccessCollection - + public var startIndex: Int { return self.items.startIndex } public var endIndex: Int { return self.items.endIndex } public func index(after i: Int) -> Int { - return self.items.index(after: i) + return self.items.index(after: i) } open subscript(index: Int) -> T { diff --git a/LeStorage/Utils/Errors.swift b/LeStorage/Utils/Errors.swift index 8fcc875..a195c94 100644 --- a/LeStorage/Utils/Errors.swift +++ b/LeStorage/Utils/Errors.swift @@ -26,7 +26,7 @@ public enum ServiceError: Error { case missingUserName case missingUserId case responseError(response: String) - case cantDecodeData(content: String) + case cantDecodeData(content: String?) } public enum UUIDError: Error {