Skip to content

Commit

Permalink
feat(pluto): add core data and keychain foundation code
Browse files Browse the repository at this point in the history
  • Loading branch information
goncalo-frade-iohk committed Nov 25, 2022
1 parent 0b8d435 commit c030400
Show file tree
Hide file tree
Showing 10 changed files with 883 additions and 1 deletion.
20 changes: 20 additions & 0 deletions Core/Sources/Base64Utils.swift
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)
}
}
97 changes: 97 additions & 0 deletions Pluto/Sources/Helpers/CoreData/CoreDataDAO+Combine.swift
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)
}
}
69 changes: 69 additions & 0 deletions Pluto/Sources/Helpers/CoreData/CoreDataDAO.swift
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)
}
}
155 changes: 155 additions & 0 deletions Pluto/Sources/Helpers/CoreData/CoreDataManager.swift
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
}
}
}
Loading

0 comments on commit c030400

Please sign in to comment.