Adds documentation

multistore
Laurent 1 year ago
parent 0cc44f46f2
commit 67516d6df6
  1. 1
      LeStorage/Services.swift
  2. 3
      LeStorage/Storable.swift
  3. 51
      LeStorage/Store.swift
  4. 52
      LeStorage/StoreCenter.swift
  5. 9
      README.md

@ -191,6 +191,7 @@ public class Services {
/// - servicePath: the path to add to the API base URL /// - servicePath: the path to add to the API base URL
/// - method: the HTTP method to execute /// - method: the HTTP method to execute
/// - requiresToken: An optional boolean to indicate if the token is required /// - requiresToken: An optional boolean to indicate if the token is required
/// - identifier: an optional StoreIdentifier that allows to filter GET requests with the StoreIdentifier values
fileprivate func _baseRequest(servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil, identifier: StoreIdentifier? = nil) throws -> URLRequest { fileprivate func _baseRequest(servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil, identifier: StoreIdentifier? = nil) throws -> URLRequest {
let urlString = baseURL + servicePath let urlString = baseURL + servicePath
guard var url = URL(string: urlString) else { guard var url = URL(string: urlString) else {

@ -31,6 +31,9 @@ public protocol Storable: Codable, Identifiable where ID : StringProtocol {
/// so when we do that on the server, we also need to do it locally /// so when we do that on the server, we also need to do it locally
func deleteDependencies() throws func deleteDependencies() throws
/// A method called to retrieve data added by the server on a POST request
/// The method will be called after a POST has succeeded,
/// and will provide a copy of what's on the server
func copyFromServerInstance(_ instance: any Storable) -> Bool func copyFromServerInstance(_ instance: any Storable) -> Bool
static var relationshipNames: [String] { get } static var relationshipNames: [String] { get }

@ -42,8 +42,10 @@ open class Store {
/// The name of the directory to store the json files /// The name of the directory to store the json files
static let storageDirectory = "storage" static let storageDirectory = "storage"
/// The store identifier, used to name the store directory, and to perform filtering requests to the server
fileprivate(set) var identifier: StoreIdentifier? = nil fileprivate(set) var identifier: StoreIdentifier? = nil
/// Indicates whether the store directory has been created at the init
fileprivate var _created: Bool = false fileprivate var _created: Bool = false
public init() { public init() {
@ -56,6 +58,9 @@ open class Store {
self._createDirectory(directory: directory) self._createDirectory(directory: directory)
} }
/// Creates the store directory
/// - Parameters:
/// - directory: the name of the directory
fileprivate func _createDirectory(directory: String) { fileprivate func _createDirectory(directory: String) {
self._created = FileManager.default.createDirectoryInDocuments(directoryName: directory) self._created = FileManager.default.createDirectoryInDocuments(directoryName: directory)
} }
@ -66,7 +71,11 @@ open class Store {
} }
/// Registers a collection /// Registers a collection
/// [synchronize] denotes a collection which modification will be sent to the django server /// - Parameters:
/// - synchronized: indicates if the data is synchronized with the server
/// - indexed: Creates an index to quickly access the data
/// - inMemory: Indicates if the collection should only live in memory, and not write into a file
/// - sendsUpdate: Indicates if updates of items should be sent to the server
public func registerCollection<T : Storable>(synchronized: Bool, indexed: Bool = false, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredCollection<T> { public func registerCollection<T : Storable>(synchronized: Bool, indexed: Bool = false, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredCollection<T> {
// register collection // register collection
@ -80,19 +89,23 @@ open class Store {
return collection return collection
} }
/// Registers a StoredSingleton instance /// Registers a singleton object
/// - Parameters:
/// - synchronized: indicates if the data is synchronized with the server
/// - inMemory: Indicates if the collection should only live in memory, and not write into a file
/// - sendsUpdate: Indicates if updates of items should be sent to the server
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredSingleton<T> { public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredSingleton<T> {
// register collection
let storedObject = StoredSingleton<T>(synchronized: synchronized, store: self, inMemory: inMemory, sendsUpdate: sendsUpdate) let storedObject = StoredSingleton<T>(synchronized: synchronized, store: self, inMemory: inMemory, sendsUpdate: sendsUpdate)
self._collections[T.resourceName()] = storedObject self._collections[T.resourceName()] = storedObject
return storedObject return storedObject
} }
// MARK: - Convenience // MARK: - Convenience
/// Looks for an instance by id /// Looks for an instance by id
/// - Parameters:
/// - id: the id of the data
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")
@ -101,7 +114,9 @@ open class Store {
return collection.findById(id) return collection.findById(id)
} }
/// Filters a collection with a [isIncluded] predicate /// Filters a collection by predicate
/// - Parameters:
/// - isIncluded: a predicate to returns if a data should be filtered in
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)
@ -141,7 +156,8 @@ open class Store {
// MARK: - Write // MARK: - Write
fileprivate func _directoryPath() throws -> URL { /// Returns the directory URL of the store
fileprivate func _directoryPath() throws -> URL {
var url = try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory) var url = try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory)
if let identifier = self.identifier?.value { if let identifier = self.identifier?.value {
url.append(component: identifier) url.append(component: identifier)
@ -149,17 +165,27 @@ open class Store {
return url return url
} }
/// Writes some content into a file inside the Store directory
/// - Parameters:
/// - content: the content to write
/// - fileName: the name of the file
func write(content: String, fileName: String) throws { func write(content: String, fileName: String) throws {
var fileURL = try self._directoryPath() var fileURL = try self._directoryPath()
fileURL.append(component: fileName) fileURL.append(component: fileName)
try content.write(to: fileURL, atomically: false, encoding: .utf8) try content.write(to: fileURL, atomically: false, encoding: .utf8)
} }
/// Returns the URL matching a Storable type
/// - Parameters:
/// - type: a Storable type
func fileURL<T: Storable>(type: T.Type) throws -> URL { func fileURL<T: Storable>(type: T.Type) throws -> URL {
let fileURL = try self._directoryPath() let fileURL = try self._directoryPath()
return fileURL.appending(component: T.fileName()) return fileURL.appending(component: T.fileName())
} }
/// Removes a file matching a Storable type
/// - Parameters:
/// - type: a Storable type
func removeFile<T: Storable>(type: T.Type) { func removeFile<T: Storable>(type: T.Type) {
do { do {
let url: URL = try self.fileURL(type: type) let url: URL = try self.fileURL(type: type)
@ -180,14 +206,23 @@ open class Store {
} }
} }
/// Requests an insertion to the StoreCenter
/// - Parameters:
/// - instance: an object to insert
func sendInsertion<T: Storable>(_ instance: T) async throws { func sendInsertion<T: Storable>(_ instance: T) async throws {
try await StoreCenter.main.sendInsertion(instance) try await StoreCenter.main.sendInsertion(instance)
} }
/// Requests an update to the StoreCenter
/// - Parameters:
/// - instance: an object to update
func sendUpdate<T: Storable>(_ instance: T) async throws { func sendUpdate<T: Storable>(_ instance: T) async throws {
try await StoreCenter.main.sendUpdate(instance) try await StoreCenter.main.sendUpdate(instance)
} }
/// Requests a deletion to the StoreCenter
/// - Parameters:
/// - instance: an object to delete
func sendDeletion<T: Storable>(_ instance: T) async throws { func sendDeletion<T: Storable>(_ instance: T) async throws {
try await StoreCenter.main.sendDeletion(instance) try await StoreCenter.main.sendDeletion(instance)
} }
@ -217,17 +252,15 @@ open class Store {
} }
extension Storable { fileprivate extension Storable {
func stringForPropertyName(_ propertyName: String) -> String? { func stringForPropertyName(_ propertyName: String) -> String? {
let mirror = Mirror(reflecting: self) let mirror = Mirror(reflecting: self)
for child in mirror.children { for child in mirror.children {
// Logger.log("child.label = \(child.label)")
if let label = child.label, label == "_\(propertyName)" { if let label = child.label, label == "_\(propertyName)" {
return child.value as? String return child.value as? String
} }
} }
Logger.log("returns nil")
return nil return nil
} }

@ -34,9 +34,11 @@ public class StoreCenter {
/// The dictionary of registered StoredCollections /// The dictionary of registered StoredCollections
fileprivate var _apiCallCollections: [String : any SomeCallCollection] = [:] fileprivate var _apiCallCollections: [String : any SomeCallCollection] = [:]
/// A collection storing FailedAPICall objects
fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil
fileprivate var blackListedUserName: [String] = [] /// A list of username that cannot synchronize with the server
fileprivate var _blackListedUserName: [String] = []
init() { init() {
self._loadExistingApiCollections() self._loadExistingApiCollections()
@ -51,6 +53,9 @@ public class StoreCenter {
} }
} }
/// Registers a store into the list of stores
/// - Parameters:
/// - store: A store to save
fileprivate func _registerStore(store: Store) { fileprivate func _registerStore(store: Store) {
guard let identifier = store.identifier?.value else { guard let identifier = store.identifier?.value else {
fatalError("The store has no identifier") fatalError("The store has no identifier")
@ -58,6 +63,10 @@ public class StoreCenter {
self._stores[identifier] = store self._stores[identifier] = store
} }
/// Returns a store using its identifier, and registers it if it does not exists
/// - Parameters:
/// - identifier: The store identifer
/// - parameter: The parameter name used to filter data on the server
public func store<T: Store>(identifier: String, parameter: String) -> T { public func store<T: Store>(identifier: String, parameter: String) -> T {
if let store = self._stores[identifier] as? T { if let store = self._stores[identifier] as? T {
return store return store
@ -140,6 +149,7 @@ public class StoreCenter {
} }
} }
/// Returns or create the ApiCall collection matching the provided T type
func getOrCreateApiCallCollection<T: Storable>() -> ApiCallCollection<T> { func getOrCreateApiCallCollection<T: Storable>() -> ApiCallCollection<T> {
if let apiCallCollection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> { if let apiCallCollection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> {
return apiCallCollection return apiCallCollection
@ -147,26 +157,38 @@ public class StoreCenter {
let apiCallCollection = ApiCallCollection<T>() let apiCallCollection = ApiCallCollection<T>()
self._apiCallCollections[T.resourceName()] = apiCallCollection self._apiCallCollections[T.resourceName()] = apiCallCollection
return apiCallCollection return apiCallCollection
// apiCallCollection.loadFromFile()
} }
/// Returns the ApiCall collection using the resource name of the provided T type
func apiCallCollection<T: Storable>() throws -> ApiCallCollection<T> { func apiCallCollection<T: Storable>() throws -> ApiCallCollection<T> {
if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> { if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> {
return collection return collection
} }
throw StoreError.collectionNotRegistered(type: T.resourceName()) throw StoreError.collectionNotRegistered(type: T.resourceName())
} }
/// Deletes an ApiCall, identifying it by dataId
/// - Parameters:
/// - type: the subsequent type of the ApiCall
/// - id: the id of the data stored inside the ApiCall
func deleteApiCallByDataId<T: Storable>(type: T.Type, id: String) async throws { func deleteApiCallByDataId<T: Storable>(type: T.Type, id: String) async throws {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
await apiCallCollection.deleteByDataId(id) await apiCallCollection.deleteByDataId(id)
} }
/// Deletes an ApiCall by its id
/// - Parameters:
/// - type: the subsequent type of the ApiCall
/// - id: the id of the ApiCall
func deleteApiCallById<T: Storable>(type: T.Type, id: String) async throws { func deleteApiCallById<T: Storable>(type: T.Type, id: String) async throws {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
await apiCallCollection.deleteById(id) await apiCallCollection.deleteById(id)
} }
/// Deletes an ApiCall by its id
/// - Parameters:
/// - id: the id of the ApiCall
/// - collectionName: the name of the collection of ApiCall
func deleteApiCallById(_ id: String, collectionName: String) async throws { func deleteApiCallById(_ id: String, collectionName: String) async throws {
if let apiCallCollection = self._apiCallCollections[collectionName] { if let apiCallCollection = self._apiCallCollections[collectionName] {
await apiCallCollection.deleteById(id) await apiCallCollection.deleteById(id)
@ -220,6 +242,9 @@ public class StoreCenter {
} }
} }
/// Resets the ApiCall whose type identifies with the provided collection
/// - Parameters:
/// - collection: The collection identifying the Storable type
public func resetApiCalls<T: Storable>(collection: StoredCollection<T>) { public func resetApiCalls<T: Storable>(collection: StoredCollection<T>) {
do { do {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
@ -303,17 +328,25 @@ public class StoreCenter {
} }
/// Adds a userName to the black list
/// Black listed username cannot send data to the server
/// - Parameters:
/// - collection: The collection identifying the Storable type
public func blackListUserName(_ userName: String) { public func blackListUserName(_ userName: String) {
self.blackListedUserName.append(userName) self._blackListedUserName.append(userName)
} }
/// Returns whether the current userName is allowed to sync with the server
func userIsAllowed() -> Bool { func userIsAllowed() -> Bool {
guard let userName = self.userName() else { guard let userName = self.userName() else {
return true 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
public func destroyStore(identifier: String) { public func destroyStore(identifier: String) {
let directory = "\(Store.storageDirectory)/\(identifier)" let directory = "\(Store.storageDirectory)/\(identifier)"
FileManager.default.deleteDirectoryInDocuments(directoryName: directory) FileManager.default.deleteDirectoryInDocuments(directoryName: directory)
@ -324,6 +357,9 @@ public class StoreCenter {
return self.collectionsCanSynchronize && self.userIsAllowed() return self.collectionsCanSynchronize && self.userIsAllowed()
} }
/// Transmit the insertion request to the ApiCall collection
/// - Parameters:
/// - instance: an object to insert
func sendInsertion<T: Storable>(_ instance: T) async throws { func sendInsertion<T: Storable>(_ instance: T) async throws {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return return
@ -331,6 +367,9 @@ public class StoreCenter {
try await self.apiCallCollection().sendInsertion(instance) try await self.apiCallCollection().sendInsertion(instance)
} }
/// Transmit the update request to the ApiCall collection
/// - Parameters:
/// - instance: an object to update
func sendUpdate<T: Storable>(_ instance: T) async throws { func sendUpdate<T: Storable>(_ instance: T) async throws {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return return
@ -338,6 +377,9 @@ public class StoreCenter {
try await self.apiCallCollection().sendUpdate(instance) try await self.apiCallCollection().sendUpdate(instance)
} }
/// Transmit the deletion request to the ApiCall collection
/// - Parameters:
/// - instance: an object to delete
func sendDeletion<T: Storable>(_ instance: T) async throws { func sendDeletion<T: Storable>(_ instance: T) async throws {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return return

@ -8,15 +8,18 @@
- To get the `StoredCollection` that manages all your cars and stores them for you, you do - To get the `StoredCollection` that manages all your cars and stores them for you, you do
`Store.main.registerCollection()` to retrieve a collection. `Store.main.registerCollection()` to retrieve a collection.
**A. Multi Store**
You can store collections inside separate folders by creating other stores
- Use StoreCenter.main.store(identifier: id, parameter: param) to create a new store. The directory will be named after the identifier. The parameter is used to retrieve data from server as the GET requests will add the parameter as an argument in the URL, like https://www.myurl.net/api/cars/?param=id
**2. Sync** **2. Sync**
- When registering your collection, you can choose to have it synchronized. To do that: - When registering your collection, you can choose to have it synchronized. To do that:
- Set `Store.main.synchronizationApiURL` - Set `StoreCenter.main.synchronizationApiURL`
- Pass `synchronized: true` when registering the collection - Pass `synchronized: true` when registering the collection
- For each of your `ModelObject`, make sure that `resourceName()` returns the resource path of the endpoint, for example "cars" - For each of your `ModelObject`, make sure that `resourceName()` returns the resource path of the endpoint, for example "cars"
- Synchronization is expected to be done with a rest_framework API on a django server - Synchronization is expected to be done with a rest_framework API on a django server
- On Django, when using cascading delete foreign, you'll want to avoid sending useless delete API calls to django, so override the `deleteDependencies` function of your ModelObject and call `deleteDependencies` on the collection for the objects you also want to delete to reproduce the cascading effect - On Django, when using cascading delete foreign, you'll want to avoid sending useless delete API calls to django, so override the `deleteDependencies` function of your ModelObject and call `deleteDependencies` on the collection for the objects you also want to delete to reproduce the cascading effect
- On your Django serializers, you want to define the following on your foreign keys to avoid having a URL instead of just the id:
`car_id = serializers.PrimaryKeyRelatedField(queryset=Car.objects.all())`

Loading…
Cancel
Save