parent
04a833b2c7
commit
daa34132c4
@ -0,0 +1,92 @@ |
||||
// |
||||
// ChatService.swift |
||||
// Chat |
||||
// |
||||
// Created by Laurent Morvillier on 11/12/2023. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
enum Method: String { |
||||
case get = "GET" |
||||
case post = "POST" |
||||
case put = "PUT" |
||||
case delete = "DELETE" |
||||
} |
||||
|
||||
enum ServiceError: Error { |
||||
case urlCreationError(url: String) |
||||
} |
||||
|
||||
class Services { |
||||
|
||||
init(url: String) { |
||||
self._baseURL = url |
||||
} |
||||
|
||||
fileprivate var _baseURL: String |
||||
|
||||
fileprivate var jsonDecoder: JSONDecoder = { |
||||
let decoder = JSONDecoder() |
||||
let dateFormatter = DateFormatter() |
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" |
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter) |
||||
return decoder |
||||
}() |
||||
|
||||
// MARK: - Base |
||||
|
||||
fileprivate func runRequest<T : Decodable>(_ request: URLRequest) async throws -> T { |
||||
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) |
||||
return try jsonDecoder.decode(T.self, from: task.0) |
||||
} |
||||
|
||||
fileprivate func getRequest(servicePath: String) throws -> URLRequest { |
||||
return try self._baseRequest(servicePath: servicePath, method: .get) |
||||
} |
||||
|
||||
fileprivate func postRequest(servicePath: String) throws -> URLRequest { |
||||
return try self._baseRequest(servicePath: servicePath, method: .post) |
||||
} |
||||
|
||||
fileprivate func putRequest(servicePath: String) throws -> URLRequest { |
||||
return try self._baseRequest(servicePath: servicePath, method: .put) |
||||
} |
||||
|
||||
fileprivate func deleteRequest(servicePath: String) throws -> URLRequest { |
||||
return try self._baseRequest(servicePath: servicePath, method: .delete) |
||||
} |
||||
|
||||
fileprivate func _baseRequest(servicePath: String, method: Method) throws -> URLRequest { |
||||
let urlString = _baseURL + servicePath |
||||
guard let url = URL(string: urlString) else { |
||||
throw ServiceError.urlCreationError(url: urlString) |
||||
} |
||||
var request = URLRequest(url: url) |
||||
request.httpMethod = method.rawValue |
||||
return request |
||||
} |
||||
|
||||
// MARK: - Services |
||||
|
||||
func get<T : Storable>() async throws -> [T] { |
||||
let getRequest = try getRequest(servicePath: T.resourceName + "/") |
||||
return try await self.runRequest(getRequest) |
||||
} |
||||
|
||||
func insert<T : Storable>(_ instance: T) async throws -> T { |
||||
let postRequest = try postRequest(servicePath: T.resourceName + "/") |
||||
return try await self.runRequest(postRequest) |
||||
} |
||||
|
||||
func update<T : Storable>(_ instance: T) async throws -> T { |
||||
let postRequest = try putRequest(servicePath: T.resourceName + "/") |
||||
return try await self.runRequest(postRequest) |
||||
} |
||||
|
||||
func delete<T : Storable>(_ instance: T) async throws -> T { |
||||
let postRequest = try deleteRequest(servicePath: T.resourceName + "/") |
||||
return try await self.runRequest(postRequest) |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,181 @@ |
||||
// |
||||
// StoredCollection.swift |
||||
// LeStorage |
||||
// |
||||
// Created by Laurent Morvillier on 02/02/2024. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
public protocol Storable : Codable, Identifiable where ID : Hashable { |
||||
static var resourceName: String { get } |
||||
} |
||||
|
||||
public class StoredCollection<T : Storable> : RandomAccessCollection, ObservableObject { |
||||
|
||||
let synchronized: Bool |
||||
|
||||
@Published public fileprivate(set) var items: [T] = [] |
||||
|
||||
fileprivate var _serviceProvider: ServiceProvider |
||||
|
||||
fileprivate var _hasChanged: Bool = false { |
||||
didSet { |
||||
if self._hasChanged == true { |
||||
self.objectWillChange.send() |
||||
self._scheduleWrite() |
||||
self._hasChanged = false |
||||
} |
||||
} |
||||
} |
||||
|
||||
init(synchronized: Bool, serviceProvider: ServiceProvider) { |
||||
self.synchronized = synchronized |
||||
self._serviceProvider = serviceProvider |
||||
self._load() |
||||
} |
||||
|
||||
fileprivate var _fileName: String { |
||||
return T.resourceName + ".json" |
||||
} |
||||
|
||||
public func addOrUpdate(instance: T) { |
||||
defer { |
||||
self._hasChanged = true |
||||
} |
||||
|
||||
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { |
||||
self.items[index] = instance |
||||
self._sendUpdateIfNecessary(instance) |
||||
} else { |
||||
self.items.append(instance) |
||||
self._sendInsertionIfNecessary(instance) |
||||
} |
||||
|
||||
} |
||||
|
||||
public func delete(instance: T) { |
||||
defer { |
||||
self._hasChanged = true |
||||
} |
||||
|
||||
self.items.removeAll { $0.id == instance.id } |
||||
self._sendDeletionIfNecessary(instance) |
||||
} |
||||
|
||||
fileprivate func _scheduleWrite() { |
||||
self._write() |
||||
} |
||||
|
||||
fileprivate func _write() { |
||||
DispatchQueue(label: "lestorage.queue.write", qos: .background).async { |
||||
do { |
||||
let jsonString = try self.items.jsonString() |
||||
let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: self._fileName) |
||||
} catch { |
||||
Logger.error(error) // TODO how to notify the main project |
||||
} |
||||
} |
||||
} |
||||
|
||||
fileprivate func _load() { |
||||
|
||||
do { |
||||
let url = try FileUtils.directoryURLForFileName(self._fileName) |
||||
if FileManager.default.fileExists(atPath: url.path()) { |
||||
self._loadAsync() |
||||
} |
||||
} catch { |
||||
Logger.log(error) |
||||
} |
||||
|
||||
} |
||||
|
||||
fileprivate func _loadAsync() { |
||||
DispatchQueue(label: "lestorage.queue.read", qos: .background).async { |
||||
|
||||
do { |
||||
let jsonString = try FileUtils.readDocumentFile(fileName: self._fileName) |
||||
if let decoded: [T] = try jsonString.decodeArray() { |
||||
DispatchQueue.main.sync { |
||||
Logger.log("loaded \(self._fileName) with \(decoded.count) items") |
||||
self.items = decoded |
||||
self.objectWillChange.send() |
||||
} |
||||
} |
||||
|
||||
} catch { |
||||
Logger.error(error) // TODO how to notify the main project |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
// MARK: - Synchronization |
||||
|
||||
fileprivate func _sendInsertionIfNecessary(_ instance: T) { |
||||
guard self.synchronized else { |
||||
return |
||||
} |
||||
|
||||
Task { |
||||
do { |
||||
let _ = try await self._serviceProvider.service?.insert(instance) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
fileprivate func _sendUpdateIfNecessary(_ instance: T) { |
||||
guard self.synchronized else { |
||||
return |
||||
} |
||||
|
||||
Task { |
||||
do { |
||||
let _ = try await self._serviceProvider.service?.insert(instance) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
fileprivate func _sendDeletionIfNecessary(_ instance: T) { |
||||
guard self.synchronized else { |
||||
return |
||||
} |
||||
|
||||
Task { |
||||
do { |
||||
let _ = try await self._serviceProvider.service?.delete(instance) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
// 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) |
||||
} |
||||
|
||||
open subscript(index: Int) -> T { |
||||
get { |
||||
return self.items[index] |
||||
} |
||||
set(newValue) { |
||||
self.items[index] = newValue |
||||
self._hasChanged = true |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
// |
||||
// Codable+Extensions.swift |
||||
// Poker Analytics 6 |
||||
// |
||||
// Created by Laurent Morvillier on 18/01/2023. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
extension Encodable { |
||||
|
||||
func jsonString() throws -> String { |
||||
let data = try self.jsonData() |
||||
return String(data: data, encoding: .utf8) ?? "" |
||||
} |
||||
|
||||
func jsonData() throws -> Data { |
||||
let encoder: JSONEncoder = JSONEncoder() |
||||
return try encoder.encode(self) |
||||
} |
||||
|
||||
func prettyJSONString() throws -> String { |
||||
let encoder: JSONEncoder = JSONEncoder() |
||||
encoder.outputFormatting = .prettyPrinted |
||||
let data = try encoder.encode(self) |
||||
return String(data: data, encoding: .utf8) ?? "" |
||||
} |
||||
|
||||
} |
||||
|
||||
extension String { |
||||
|
||||
func decode<T : Decodable>() throws -> T? { |
||||
return try self.data(using: .utf8)?.decode() |
||||
} |
||||
|
||||
func decodeArray<T : Decodable>() throws -> [T]? { |
||||
return try self.data(using: .utf8)?.decodeArray() |
||||
} |
||||
|
||||
} |
||||
|
||||
extension Data { |
||||
|
||||
func decode<T : Decodable>() throws -> T { |
||||
return try JSONDecoder().decode(T.self, from: self) |
||||
} |
||||
|
||||
func decodeArray<T : Decodable>() throws -> [T] { |
||||
return try JSONDecoder().decode([T].self, from: self) |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
// |
||||
// FileWriter.swift |
||||
// Poker Analytics 4 |
||||
// |
||||
// Created by Laurent Morvillier on 04/09/2018. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
enum FileError : Error { |
||||
case documentDirectoryNotFound |
||||
} |
||||
|
||||
enum FileFormat { |
||||
case csv |
||||
case html |
||||
} |
||||
|
||||
class FileUtils { |
||||
|
||||
static func pathsFromDocumentsDirectory() throws -> [String] { |
||||
let documentsURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
||||
return try FileManager.default.contentsOfDirectory(atPath: documentsURL.path) |
||||
} |
||||
|
||||
static func readDocumentFile(fileName: String) throws -> String { |
||||
let fileURL: URL = try self.directoryURLForFileName(fileName) |
||||
// Logger.log("url = \(fileURL.absoluteString)") |
||||
return try String(contentsOf: fileURL, encoding: .utf8) |
||||
|
||||
// if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
||||
// let fileURL: URL = dir.appendingPathComponent(fileName) |
||||
// Logger.log("url = \(fileURL.absoluteString)") |
||||
// return try String(contentsOf: fileURL, encoding: .utf8) |
||||
// } |
||||
// throw FileError.documentDirectoryNotFound |
||||
} |
||||
|
||||
static func readFile(fileURL: URL) throws -> String { |
||||
return try String(contentsOf: fileURL, encoding: .utf8) |
||||
} |
||||
|
||||
static func directoryURLForFileName(_ fileName: String) throws -> URL { |
||||
if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
||||
return dir.appendingPathComponent(fileName) |
||||
} |
||||
throw FileError.documentDirectoryNotFound |
||||
} |
||||
|
||||
static func writeToDocumentDirectory(content: String, fileName: String) throws -> URL { |
||||
|
||||
if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
||||
let fileURL: URL = dir.appendingPathComponent(fileName) |
||||
try content.write(to: fileURL, atomically: false, encoding: .utf8) |
||||
return fileURL |
||||
} |
||||
throw FileError.documentDirectoryNotFound |
||||
} |
||||
|
||||
@discardableResult static func writeToDocumentDirectory(data: Data, fileName: String) throws -> URL { |
||||
|
||||
if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
||||
let fileURL: URL = dir.appendingPathComponent(fileName) |
||||
try data.write(to: fileURL) |
||||
return fileURL |
||||
} |
||||
throw FileError.documentDirectoryNotFound |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,65 @@ |
||||
// |
||||
// Logger.swift |
||||
// Poker Analytics 4 |
||||
// |
||||
// Created by Laurent Morvillier on 29/03/2018. |
||||
// |
||||
|
||||
import Foundation |
||||
#if !DEBUG |
||||
import Firebase |
||||
#endif |
||||
|
||||
@objc public class Logger : NSObject { |
||||
|
||||
@objc static public func log(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) { |
||||
let filestr: NSString = NSString(string: file) |
||||
print("\(filestr.lastPathComponent).\(line).\(function): \(message)") |
||||
} |
||||
|
||||
@objc static public func error(_ error: Error) { |
||||
Logger.error(error, file: #file, function: #function, line: #line) |
||||
} |
||||
|
||||
static public func error(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) { |
||||
let filestr: NSString = NSString(string: file) |
||||
var fireBaseError: Error { |
||||
if let customError = error as? CustomNSError & LocalizedError { |
||||
return customError.fireBaseError |
||||
} else { |
||||
return error |
||||
} |
||||
} |
||||
print("ERROR: \(filestr.lastPathComponent).\(line).\(function): \(fireBaseError)") |
||||
} |
||||
|
||||
@objc static public func w(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) { |
||||
let filestr: NSString = NSString(string: file) |
||||
print("Warning: \(filestr.lastPathComponent).\(line).\(function): \(message)") |
||||
} |
||||
|
||||
@objc static public func crashLogging(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { |
||||
let fileName: String = file.components(separatedBy: "/").last ?? file |
||||
#if DEBUG |
||||
NSLogv("%@.%i.%@: %@", getVaList([fileName, line, function, message])) |
||||
#else |
||||
Crashlytics.crashlytics().log(format: "%@.%i.%@: %@", arguments: getVaList([fileName, line, function, message])) |
||||
#endif |
||||
} |
||||
|
||||
} |
||||
|
||||
extension LocalizedError where Self: CustomNSError { |
||||
var simpleErrorDescription: String { |
||||
let mirror = Mirror(reflecting: self) |
||||
if let label = mirror.children.first?.label { |
||||
return label |
||||
} else { |
||||
return String(describing:self) |
||||
} |
||||
} |
||||
|
||||
var fireBaseError: NSError { |
||||
NSError(domain: Self.errorDomain + "." + self.simpleErrorDescription, code: self.errorCode, userInfo: self.errorUserInfo) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue