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
/// - method: the HTTP method to execute
/// - 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 {
let urlString = baseURL + servicePath
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
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
static var relationshipNames: [String] { get }

@ -42,8 +42,10 @@ open class Store {
/// The name of the directory to store the json files
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
/// Indicates whether the store directory has been created at the init
fileprivate var _created: Bool = false
public init() {
@ -56,6 +58,9 @@ open class Store {
self._createDirectory(directory: directory)
}
/// Creates the store directory
/// - Parameters:
/// - directory: the name of the directory
fileprivate func _createDirectory(directory: String) {
self._created = FileManager.default.createDirectoryInDocuments(directoryName: directory)
}
@ -66,7 +71,11 @@ open class Store {
}
/// 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> {
// register collection
@ -80,19 +89,23 @@ open class Store {
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> {
// register collection
let storedObject = StoredSingleton<T>(synchronized: synchronized, store: self, inMemory: inMemory, sendsUpdate: sendsUpdate)
self._collections[T.resourceName()] = storedObject
return storedObject
}
// MARK: - Convenience
/// Looks for an instance by id
/// - Parameters:
/// - id: the id of the data
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")
@ -101,7 +114,9 @@ open class Store {
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] {
do {
return try self.collection().filter(isIncluded)
@ -141,7 +156,8 @@ open class Store {
// 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)
if let identifier = self.identifier?.value {
url.append(component: identifier)
@ -149,17 +165,27 @@ open class Store {
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 {
var fileURL = try self._directoryPath()
fileURL.append(component: fileName)
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 {
let fileURL = try self._directoryPath()
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) {
do {
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 {
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 {
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 {
try await StoreCenter.main.sendDeletion(instance)
}
@ -217,17 +252,15 @@ open class Store {
}
extension Storable {
fileprivate extension Storable {
func stringForPropertyName(_ propertyName: String) -> String? {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
// Logger.log("child.label = \(child.label)")
if let label = child.label, label == "_\(propertyName)" {
return child.value as? String
}
}
Logger.log("returns nil")
return nil
}

@ -34,9 +34,11 @@ public class StoreCenter {
/// The dictionary of registered StoredCollections
fileprivate var _apiCallCollections: [String : any SomeCallCollection] = [:]
/// A collection storing FailedAPICall objects
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() {
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) {
guard let identifier = store.identifier?.value else {
fatalError("The store has no identifier")
@ -58,6 +63,10 @@ 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
/// - parameter: The parameter name used to filter data on the server
public func store<T: Store>(identifier: String, parameter: String) -> T {
if let store = self._stores[identifier] as? T {
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> {
if let apiCallCollection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> {
return apiCallCollection
@ -147,26 +157,38 @@ public class StoreCenter {
let apiCallCollection = ApiCallCollection<T>()
self._apiCallCollections[T.resourceName()] = 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> {
if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> {
return collection
}
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 {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
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 {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
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 {
if let apiCallCollection = self._apiCallCollections[collectionName] {
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>) {
do {
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) {
self.blackListedUserName.append(userName)
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
public func destroyStore(identifier: String) {
let directory = "\(Store.storageDirectory)/\(identifier)"
FileManager.default.deleteDirectoryInDocuments(directoryName: directory)
@ -324,6 +357,9 @@ public class StoreCenter {
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 {
guard self._canSynchronise() else {
return
@ -331,6 +367,9 @@ public class StoreCenter {
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 {
guard self._canSynchronise() else {
return
@ -338,6 +377,9 @@ public class StoreCenter {
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 {
guard self._canSynchronise() else {
return

@ -8,15 +8,18 @@
- To get the `StoredCollection` that manages all your cars and stores them for you, you do
`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**
- 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
- 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
- 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