-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pluto): add core data and keychain foundation code
- Loading branch information
1 parent
0b8d435
commit c030400
Showing
10 changed files
with
883 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import Foundation | ||
|
||
public struct Base64Utils { | ||
public init() {} | ||
|
||
public func encode(_ data: Data) -> String { | ||
String(data.base64EncodedString() | ||
.replacingOccurrences(of: "/", with: "_") | ||
.replacingOccurrences(of: "+", with: "-") | ||
.dropLast(1)) | ||
} | ||
|
||
public func decode(_ src: String) -> Data? { | ||
let base64Encoded = src | ||
.replacingOccurrences(of: "_", with: "/") | ||
.replacingOccurrences(of: "-", with: "+") | ||
+ "=" | ||
return Data(base64Encoded: base64Encoded) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import Combine | ||
import CoreData | ||
import Foundation | ||
|
||
extension CoreDataDAO { | ||
func fetchController( | ||
predicate: NSPredicate? = nil, | ||
sorting: NSSortDescriptor? = nil, | ||
fetchLimit: Int? = nil, | ||
context: NSManagedObjectContext | ||
) -> AnyPublisher<[CoreDataObject], Error> { | ||
let request = CoreDataObject.fetchRequest() | ||
request.predicate = predicate | ||
request.sortDescriptors = sorting.map { [$0] } ?? [NSSortDescriptor(key: identifierKey, ascending: true)] | ||
fetchLimit.map { request.fetchLimit = $0 } | ||
|
||
return context | ||
.fetchPublisher(request: request) | ||
.map { $0 as? [CoreDataObject] ?? [] } | ||
.map { | ||
let unique = Set($0) | ||
return Array(unique) | ||
} | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
func fetchByKeyValuePublisher( | ||
key: String, | ||
value: CustomStringConvertible, | ||
context: NSManagedObjectContext | ||
) -> AnyPublisher<[CoreDataObject], Error> { | ||
return fetchController( | ||
predicate: NSPredicate(format: "%K == %@", key, "\(value)"), | ||
context: context | ||
) | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
func deletePublisher(predicate: NSPredicate? = nil, context: NSManagedObjectContext) -> AnyPublisher<Void, Error> { | ||
return context.write { | ||
try delete(predicate: predicate, context: $0) | ||
} | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
func deleteAllPublisher(context: NSManagedObjectContext) -> AnyPublisher<Void, Error> { | ||
return context.write { | ||
try deleteAll(context: $0) | ||
} | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
extension CoreDataDAO where CoreDataObject: Identifiable { | ||
func updateOrCreate( | ||
_ id: CoreDataObject.ID, | ||
context: NSManagedObjectContext, | ||
modify: @escaping (CoreDataObject, NSManagedObjectContext) -> Void | ||
) -> AnyPublisher<CoreDataObject.ID, Error> { | ||
context.write { context in | ||
modify(self.fetchByID(id, context: context) ?? self.newEntity(context: context), context) | ||
return id | ||
} | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
func fetchByIDsPublisher( | ||
_ identifier: CoreDataObject.ID, | ||
context: NSManagedObjectContext | ||
) -> AnyPublisher<CoreDataObject?, Error> { | ||
guard let key = identifierKey else { | ||
assertionFailure(""" | ||
The identityKey is nil, please set up the identityKey when you have an Identifiable object | ||
""") | ||
return Just(nil).tryMap { $0 }.eraseToAnyPublisher() | ||
} | ||
return fetchController( | ||
predicate: NSPredicate(format: "%K == %@", key, "\(identifier)"), | ||
context: context | ||
) | ||
.map { $0.first } | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
func deleteByIDsPublisher( | ||
_ identifiers: [CoreDataObject.ID], | ||
context: NSManagedObjectContext | ||
) -> AnyPublisher<Void, Error> { | ||
guard let key = identifierKey else { | ||
assertionFailure(""" | ||
The identityKey is nil, please set up the identityKey when you have an Identifiable object | ||
""") | ||
return Just(()).tryMap { $0 }.eraseToAnyPublisher() | ||
} | ||
return deletePublisher(predicate: NSPredicate(format: "\(key) IN %@", identifiers), context: context) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import CoreData | ||
|
||
protocol CoreDataDAO { | ||
associatedtype CoreDataObject: NSManagedObject | ||
var identifierKey: String? { get } | ||
} | ||
|
||
extension CoreDataDAO { | ||
func newEntity(context: NSManagedObjectContext) -> CoreDataObject { | ||
CoreDataObject(entity: CoreDataObject.entity(), insertInto: context) | ||
} | ||
|
||
func fetch( | ||
predicate: NSPredicate? = nil, | ||
sorting: NSSortDescriptor? = nil, | ||
fetchLimit: Int? = nil, | ||
context: NSManagedObjectContext | ||
) -> Set<CoreDataObject> { | ||
let request = CoreDataObject.fetchRequest() | ||
request.predicate = predicate | ||
fetchLimit.map { request.fetchLimit = $0 } | ||
sorting.map { request.sortDescriptors = [$0] } | ||
let items = (try? context.fetch(request) as? [CoreDataObject]) ?? [] | ||
return Set(items) | ||
} | ||
|
||
func delete(predicate: NSPredicate? = nil, context: NSManagedObjectContext) throws { | ||
let fetchRequest = CoreDataObject.fetchRequest() | ||
fetchRequest.predicate = predicate | ||
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) | ||
batchDeleteRequest.resultType = .resultTypeObjectIDs | ||
guard | ||
let result = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult, | ||
let objects = result.result as? [NSManagedObjectID] | ||
else { | ||
return | ||
} | ||
let changes: [AnyHashable: Any] = [ | ||
NSDeletedObjectsKey: objects | ||
] | ||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) | ||
} | ||
|
||
func deleteAll(context: NSManagedObjectContext) throws { | ||
try delete(predicate: nil, context: context) | ||
} | ||
} | ||
|
||
extension CoreDataDAO where CoreDataObject: Identifiable { | ||
func fetchByID(_ identifier: CoreDataObject.ID, context: NSManagedObjectContext) -> CoreDataObject? { | ||
guard let key = identifierKey else { | ||
assertionFailure(""" | ||
The identityKey is nil, please set up the identityKey when you have an Identifiable object | ||
""") | ||
return nil | ||
} | ||
return fetch(predicate: NSPredicate(format: "%K == %@", key, "\(identifier)"), context: context).first | ||
} | ||
|
||
func deleteByID(_ identifier: CoreDataObject.ID, context: NSManagedObjectContext) throws { | ||
guard let key = identifierKey else { | ||
assertionFailure(""" | ||
The identityKey is nil, please set up the identityKey when you have an Identifiable object | ||
""") | ||
return | ||
} | ||
try delete(predicate: NSPredicate(format: "%K == %@", key, "\(identifier)"), context: context) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import CoreData | ||
import Foundation | ||
|
||
public final class CoreDataManager { | ||
lazy var editContext: NSManagedObjectContext = persistentContainer.newBackgroundContext() | ||
|
||
lazy var mainContext: NSManagedObjectContext = { | ||
func mergeChanges(context: NSManagedObjectContext, notification: Notification) { | ||
guard | ||
let contextSaved = notification.object as? NSManagedObjectContext, | ||
self.editContext == contextSaved | ||
else { return } | ||
context.perform { | ||
context.mergeChanges(fromContextDidSave: notification) | ||
} | ||
} | ||
|
||
let context = persistentContainer.viewContext | ||
context.automaticallyMergesChangesFromParent = true | ||
|
||
NotificationCenter.default.addObserver( | ||
forName: .NSManagedObjectContextDidMergeChangesObjectIDs, | ||
object: nil, | ||
queue: nil | ||
) { [weak context] notification in | ||
guard let context = context else { return } | ||
mergeChanges(context: context, notification: notification) | ||
} | ||
|
||
return context | ||
}() | ||
|
||
private let setup: CoreDataSetup | ||
|
||
private static var _model: NSManagedObjectModel? | ||
|
||
private(set) lazy var model: NSManagedObjectModel = { | ||
if let mdl = CoreDataManager._model { return mdl } | ||
let modelPath: URL | ||
switch setup.modelPath { | ||
case let .storeName(name): | ||
guard let modelURL = Bundle(for: type(of: self)).url(forResource: name, withExtension: "momd") else { | ||
fatalError("Unable to Find Data Model") | ||
} | ||
modelPath = modelURL | ||
case let .storeURL(modelURL): | ||
modelPath = modelURL | ||
} | ||
|
||
guard let managedObjectModel = NSManagedObjectModel(contentsOf: modelPath) else { | ||
fatalError("Unable to Load Data Model") | ||
} | ||
CoreDataManager._model = managedObjectModel | ||
return managedObjectModel | ||
}() | ||
|
||
private(set) lazy var persistentContainer: NSPersistentContainer = { | ||
let persistentContainer = NSPersistentContainer(name: self.setup.modelName, managedObjectModel: model) | ||
if case CoreDataSetup.StoreType.memory = setup.storeType { | ||
let persistentStoreDescription = NSPersistentStoreDescription() | ||
persistentStoreDescription.type = NSInMemoryStoreType | ||
persistentContainer.persistentStoreDescriptions = [persistentStoreDescription] | ||
} | ||
persistentContainer.loadPersistentStores { [weak persistentContainer] _, _ in | ||
persistentContainer?.viewContext.automaticallyMergesChangesFromParent = true | ||
} | ||
return persistentContainer | ||
}() | ||
|
||
private(set) lazy var defaultManagedObjectContext: NSManagedObjectContext = self.persistentContainer.viewContext | ||
|
||
public init(setup: CoreDataSetup) { | ||
self.setup = setup | ||
} | ||
|
||
public func start() { | ||
persistentContainer.loadPersistentStores { _, error in | ||
error.map { assertionFailure($0.localizedDescription) } | ||
} | ||
} | ||
|
||
public func delete() throws { | ||
try persistentContainer | ||
.persistentStoreCoordinator | ||
.destroyPersistentStore( | ||
at: persistentStoreUrl(), | ||
ofType: "sqlite", | ||
options: nil | ||
) | ||
start() | ||
} | ||
|
||
private func makeNewContext( | ||
concurrencyType: NSManagedObjectContextConcurrencyType | ||
) -> NSManagedObjectContext { | ||
let context = NSManagedObjectContext(concurrencyType: concurrencyType) | ||
context.parent = defaultManagedObjectContext | ||
context.automaticallyMergesChangesFromParent = true | ||
return context | ||
} | ||
|
||
private func persistentStoreUrl() -> URL { | ||
let modelName: String | ||
switch setup.modelPath { | ||
case let .storeName(value): | ||
modelName = value | ||
case let .storeURL(value): | ||
modelName = value.deletingPathExtension().lastPathComponent | ||
} | ||
let url = FileManager.default | ||
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] | ||
.appendingPathComponent("\(modelName).sqlite") | ||
|
||
assert(FileManager.default.fileExists(atPath: url.path)) | ||
|
||
return url | ||
} | ||
} | ||
|
||
public extension CoreDataManager { | ||
struct CoreDataSetup { | ||
public enum StoreType { | ||
case memory | ||
case persistent | ||
} | ||
|
||
public enum ModelPath { | ||
case storeName(String) | ||
case storeURL(URL) | ||
} | ||
|
||
// swiftlint:disable lower_acl_than_parent | ||
|
||
public let modelPath: ModelPath | ||
public let storeType: StoreType | ||
|
||
public init(modelPath: ModelPath, storeType: StoreType) { | ||
self.modelPath = modelPath | ||
self.storeType = storeType | ||
} | ||
|
||
// swiftlint:enable lower_acl_than_parent | ||
} | ||
} | ||
|
||
private extension CoreDataManager.CoreDataSetup { | ||
var modelName: String { | ||
switch modelPath { | ||
case let .storeName(name): | ||
return name | ||
case let .storeURL(url): | ||
return url.deletingPathExtension().lastPathComponent | ||
} | ||
} | ||
} |
Oops, something went wrong.