From 65b2f751602a8bcbec34ccec02e2694783144417 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 18 Sep 2024 15:23:12 +0200 Subject: [PATCH] Accept storable ids to not only be strings --- LeStorage.xcodeproj/project.pbxproj | 78 +++++++++---------- .../xcschemes/LeStorageTests.xcscheme | 55 +++++++++++++ LeStorage/ApiCallCollection.swift | 4 +- LeStorage/Services.swift | 7 +- LeStorage/Storable.swift | 13 +++- LeStorage/Store.swift | 2 +- LeStorage/StoredCollection.swift | 16 ++-- LeStorageTests/LeStorageTests.swift | 68 ++++++++++++++++ 8 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorageTests.xcscheme create mode 100644 LeStorageTests/LeStorageTests.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index f3c47ee..0a30bde 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */ = {isa = PBXBuildFile; fileRef = C425D4382B6D24E1002A7B48 /* LeStorage.docc */; }; - C425D43F2B6D24E1002A7B48 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4342B6D24E1002A7B48 /* LeStorage.framework */; }; - C425D4442B6D24E1002A7B48 /* LeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */; }; 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 */; }; C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredSingleton.swift */; }; @@ -31,12 +29,13 @@ C4A47D9B2B7CFFDA00ADC637 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D992B7CFFC500ADC637 /* ApiCall.swift */; }; C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D9A2B7CFFC500ADC637 /* Settings.swift */; }; C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAE2B85FD3800ADC637 /* Errors.swift */; }; + C4C33F6F2C9B06B7006316DE /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4342B6D24E1002A7B48 /* LeStorage.framework */; }; C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */; }; C4FC2E312C353E7B0021F3BF /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E302C353E7B0021F3BF /* Log.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - C425D4402B6D24E1002A7B48 /* PBXContainerItemProxy */ = { + C4C33F702C9B06B7006316DE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C425D42B2B6D24E1002A7B48 /* Project object */; proxyType = 1; @@ -49,8 +48,6 @@ 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 = ""; }; - C425D43E2B6D24E1002A7B48 /* LeStorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeStorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeStorageTests.swift; sourceTree = ""; }; C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = ""; }; C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = ""; }; @@ -71,10 +68,15 @@ C4A47D992B7CFFC500ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; C4A47D9A2B7CFFC500ADC637 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; C4A47DAE2B85FD3800ADC637 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + C4C33F6B2C9B06B7006316DE /* LeStorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeStorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCenter.swift; sourceTree = ""; }; C4FC2E302C353E7B0021F3BF /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + C4C33F6C2C9B06B7006316DE /* LeStorageTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LeStorageTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ C425D4312B6D24E1002A7B48 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -83,11 +85,11 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C425D43B2B6D24E1002A7B48 /* Frameworks */ = { + C4C33F682C9B06B7006316DE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C425D43F2B6D24E1002A7B48 /* LeStorage.framework in Frameworks */, + C4C33F6F2C9B06B7006316DE /* LeStorage.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -98,7 +100,7 @@ isa = PBXGroup; children = ( C425D4362B6D24E1002A7B48 /* LeStorage */, - C425D4422B6D24E1002A7B48 /* LeStorageTests */, + C4C33F6C2C9B06B7006316DE /* LeStorageTests */, C425D4352B6D24E1002A7B48 /* Products */, ); sourceTree = ""; @@ -107,7 +109,7 @@ isa = PBXGroup; children = ( C425D4342B6D24E1002A7B48 /* LeStorage.framework */, - C425D43E2B6D24E1002A7B48 /* LeStorageTests.xctest */, + C4C33F6B2C9B06B7006316DE /* LeStorageTests.xctest */, ); name = Products; sourceTree = ""; @@ -134,14 +136,6 @@ path = LeStorage; sourceTree = ""; }; - C425D4422B6D24E1002A7B48 /* LeStorageTests */ = { - isa = PBXGroup; - children = ( - C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */, - ); - path = LeStorageTests; - sourceTree = ""; - }; C4A47D582B6D352900ADC637 /* Utils */ = { isa = PBXGroup; children = ( @@ -207,22 +201,27 @@ productReference = C425D4342B6D24E1002A7B48 /* LeStorage.framework */; productType = "com.apple.product-type.framework"; }; - C425D43D2B6D24E1002A7B48 /* LeStorageTests */ = { + C4C33F6A2C9B06B7006316DE /* LeStorageTests */ = { isa = PBXNativeTarget; - buildConfigurationList = C425D44B2B6D24E1002A7B48 /* Build configuration list for PBXNativeTarget "LeStorageTests" */; + buildConfigurationList = C4C33F722C9B06B7006316DE /* Build configuration list for PBXNativeTarget "LeStorageTests" */; buildPhases = ( - C425D43A2B6D24E1002A7B48 /* Sources */, - C425D43B2B6D24E1002A7B48 /* Frameworks */, - C425D43C2B6D24E1002A7B48 /* Resources */, + C4C33F672C9B06B7006316DE /* Sources */, + C4C33F682C9B06B7006316DE /* Frameworks */, + C4C33F692C9B06B7006316DE /* Resources */, ); buildRules = ( ); dependencies = ( - C425D4412B6D24E1002A7B48 /* PBXTargetDependency */, + C4C33F712C9B06B7006316DE /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + C4C33F6C2C9B06B7006316DE /* LeStorageTests */, ); name = LeStorageTests; + packageProductDependencies = ( + ); productName = LeStorageTests; - productReference = C425D43E2B6D24E1002A7B48 /* LeStorageTests.xctest */; + productReference = C4C33F6B2C9B06B7006316DE /* LeStorageTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -232,14 +231,14 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1520; TargetAttributes = { C425D4332B6D24E1002A7B48 = { CreatedOnToolsVersion = 15.2; }; - C425D43D2B6D24E1002A7B48 = { - CreatedOnToolsVersion = 15.2; + C4C33F6A2C9B06B7006316DE = { + CreatedOnToolsVersion = 16.0; }; }; }; @@ -257,7 +256,7 @@ projectRoot = ""; targets = ( C425D4332B6D24E1002A7B48 /* LeStorage */, - C425D43D2B6D24E1002A7B48 /* LeStorageTests */, + C4C33F6A2C9B06B7006316DE /* LeStorageTests */, ); }; /* End PBXProject section */ @@ -271,7 +270,7 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C425D43C2B6D24E1002A7B48 /* Resources */ = { + C4C33F692C9B06B7006316DE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -310,21 +309,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C425D43A2B6D24E1002A7B48 /* Sources */ = { + C4C33F672C9B06B7006316DE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C425D4442B6D24E1002A7B48 /* LeStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - C425D4412B6D24E1002A7B48 /* PBXTargetDependency */ = { + C4C33F712C9B06B7006316DE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C425D4332B6D24E1002A7B48 /* LeStorage */; - targetProxy = C425D4402B6D24E1002A7B48 /* PBXContainerItemProxy */; + targetProxy = C4C33F702C9B06B7006316DE /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -518,14 +516,14 @@ }; name = Release; }; - C425D44C2B6D24E1002A7B48 /* Debug */ = { + C4C33F732C9B06B7006316DE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 526E96RFNP; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeStorageTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -535,14 +533,14 @@ }; name = Debug; }; - C425D44D2B6D24E1002A7B48 /* Release */ = { + C4C33F742C9B06B7006316DE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 526E96RFNP; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeStorageTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -573,11 +571,11 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C425D44B2B6D24E1002A7B48 /* Build configuration list for PBXNativeTarget "LeStorageTests" */ = { + C4C33F722C9B06B7006316DE /* Build configuration list for PBXNativeTarget "LeStorageTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - C425D44C2B6D24E1002A7B48 /* Debug */, - C425D44D2B6D24E1002A7B48 /* Release */, + C4C33F732C9B06B7006316DE /* Debug */, + C4C33F742C9B06B7006316DE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorageTests.xcscheme b/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorageTests.xcscheme new file mode 100644 index 0000000..2dc8d3e --- /dev/null +++ b/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorageTests.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index bf25ce0..0c4d979 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -182,7 +182,7 @@ actor ApiCallCollection: SomeCallCollection { /// The method updates existing calls or creates a new one fileprivate func _callForInstance(_ instance: T, method: HTTPMethod) throws -> ApiCall? { - if let existingCall = self.items.first(where: { $0.dataId == instance.id }) { + if let existingCall = self.items.first(where: { $0.dataId == instance.stringId }) { switch method { case .delete: self.deleteById(existingCall.id) // delete the existing call as we don't need it @@ -203,7 +203,7 @@ actor ApiCallCollection: SomeCallCollection { /// Creates an API call for the Storable [instance] and an HTTP [method] fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall { let jsonString = try instance.jsonString() - return ApiCall(method: method, dataId: String(instance.id), body: jsonString) + return ApiCall(method: method, dataId: instance.stringId, body: jsonString) } /// Prepares a call for execution by updating its properties and adding it to its collection for storage diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 84b9c50..b70b301 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -111,13 +111,14 @@ public class Services { /// - request: the URLRequest to run /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { - Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") + let debugURL = request.url?.absoluteString ?? "" + print("Run \(request.httpMethod ?? "") \(debugURL)") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) - Logger.log("response = \(String(data: task.0, encoding: .utf8) ?? "")") + print("response = \(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)") + print("\(debugURL) ended, status code = \(statusCode)") switch statusCode { case 200..<300: // success if let apiCallId { diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index a3d61c4..9309ff2 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -8,7 +8,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 where ID : StringProtocol { +public protocol Storable: Codable, Identifiable { /// The store containing a reference to the instance var store: Store? { get set } @@ -48,7 +48,16 @@ extension Storable { } /// Returns a string id for the instance - var stringId: String { return String(self.id) } + var stringId: String { + switch self.id { + case let sp as any StringProtocol: + return String(sp) + case let intLitteral as any ExpressibleByIntegerLiteral: + return "\(intLitteral)" + default: + fatalError("id not convertible to string") + } + } /// Returns the relative path of the instance for the django server static func path(id: String? = nil) -> String { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 00c50e7..824143b 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -114,7 +114,7 @@ open class Store { /// Looks for an instance by id /// - Parameters: /// - id: the id of the data - public func findById(_ id: String) -> T? { + public func findById(_ id: T.ID) -> T? { guard let collection = self._collections[T.resourceName()] as? StoredCollection else { Logger.w("Collection \(T.resourceName()) not registered") return nil diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 91070ad..17a2d65 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -55,7 +55,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti fileprivate var _store: Store /// Provides fast access for instances if the collection has been instanced with [indexed] = true - fileprivate var _indexes: [String : 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 { @@ -178,7 +178,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti /// Updates the whole index with the items array fileprivate func _updateIndexIfNecessary() { if let _ = self._indexes { - self._indexes = self.items.dictionary { $0.stringId } + self._indexes = self.items.dictionary { $0.id } } } @@ -229,7 +229,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.items.append(instance) self._sendInsertionIfNecessary(instance) } - self._indexes?[instance.stringId] = instance + self._indexes?[instance.id] = instance } @@ -259,7 +259,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } - self._indexes?.removeValue(forKey: instance.stringId) + self._indexes?.removeValue(forKey: instance.id) self._sendDeletionIfNecessary(instance) } @@ -274,7 +274,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti for instance in sequence { try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } - self._indexes?.removeValue(forKey: instance.stringId) + self._indexes?.removeValue(forKey: instance.id) self._sendDeletionIfNecessary(instance) } } @@ -308,13 +308,13 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } } instance.store = self._store - self._indexes?[instance.stringId] = instance + self._indexes?[instance.id] = instance } } /// Returns the instance corresponding to the provided [id] - public func findById(_ id: String) -> T? { + public func findById(_ id: T.ID) -> T? { if let index = self._indexes, let instance = index[id] { return instance } @@ -322,7 +322,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } /// Deletes the instance corresponding to the provided [id] - public func deleteById(_ id: String) throws { + public func deleteById(_ id: T.ID) throws { if let instance = self.findById(id) { try self.delete(instance: instance) } diff --git a/LeStorageTests/LeStorageTests.swift b/LeStorageTests/LeStorageTests.swift new file mode 100644 index 0000000..bbd46ae --- /dev/null +++ b/LeStorageTests/LeStorageTests.swift @@ -0,0 +1,68 @@ +// +// LeStorageTests.swift +// LeStorageTests +// +// Created by Laurent Morvillier on 18/09/2024. +// + +import Testing +import LeStorage + +class IntObject: ModelObject, Storable { + static func resourceName() -> String { "int" } + static func tokenExemptedMethods() -> [LeStorage.HTTPMethod] { [] } + static func filterByStoreIdentifier() -> Bool { false } + static var relationshipNames: [String] = [] + + var id: Int + var name: String + + init(id: Int, name: String) { + self.id = id + self.name = name + } +} + +class StringObject: ModelObject, Storable { + static func resourceName() -> String { "string" } + static func tokenExemptedMethods() -> [LeStorage.HTTPMethod] { [] } + static func filterByStoreIdentifier() -> Bool { false } + static var relationshipNames: [String] = [] + + var id: String + var name: String + + init(id: String, name: String) { + self.id = id + self.name = name + } +} + +struct LeStorageTests { + + @Test func testIntIds() async throws { + let intObjects: StoredCollection = Store.main.registerCollection(synchronized: false) + + let int = IntObject(id: 12, name: "test") + try? intObjects.addOrUpdate(instance: int) + + if let search = intObjects.findById(12) { + #expect(search.id == 12) + } else { + Issue.record("object is missing") + } + } + + @Test func testStringIds() async throws { + let stringObjects: StoredCollection = Store.main.registerCollection(synchronized: false) + + let string = StringObject(id: "coco", name: "name") + try? stringObjects.addOrUpdate(instance: string) + + if let search = stringObjects.findById("coco") { + #expect(search.id == "coco") + } else { + Issue.record("object is missing") + } + } +}