Last active
June 18, 2020 08:00
-
-
Save KaQuMiQ/025a4e4d384b1d4dbe0086818ed9033d to your computer and use it in GitHub Desktop.
CodeData - CoreData from code
This file contains hidden or 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
| // MARK: - Storage.swift | |
| import CoreData | |
| public final class Storage { | |
| private let queue: DispatchQueue | |
| private let context: NSManagedObjectContext | |
| private let schema: StorageSchema | |
| public enum StoreType { | |
| case sqlite | |
| case inMemory | |
| internal var coreDataType: String { | |
| switch self { | |
| case .sqlite: return NSSQLiteStoreType | |
| case .inMemory: return NSInMemoryStoreType | |
| } | |
| } | |
| } | |
| public init( | |
| name: String, | |
| store: StoreType = .sqlite, | |
| schema: StorageSchema, | |
| migrations: Array<StorageMigration> | |
| ) throws { | |
| do { | |
| let storeURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("\(name).sqlite") | |
| if case .sqlite = store { | |
| try NSKeyedArchiver.archivedData(withRootObject: schema.managedObjectModel, requiringSecureCoding: true).write(to: storeURL.deletingLastPathComponent().appendingPathComponent("CodeDataModel_\(schema.version)")) | |
| try Storage.migrateIfNeeded(storeURL, type: store, migrations: migrations) | |
| } else { /**/ } | |
| self.queue = DispatchQueue(label: "codeData.storage.\(name)") | |
| self.schema = schema | |
| let storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: schema.managedObjectModel) | |
| try storeCoordinator | |
| .addPersistentStore( | |
| ofType: store.coreDataType, | |
| configurationName: nil, | |
| at: storeURL, | |
| options: [NSIgnorePersistentStoreVersioningOption: true] | |
| ) | |
| if case .sqlite = store { | |
| try NSPersistentStoreCoordinator.setMetadata(["SchemaVersion" : schema.version], forPersistentStoreOfType: store.coreDataType, at: storeURL, options: nil) | |
| } else { /**/ } | |
| self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) | |
| self.context.mergePolicy = NSOverwriteMergePolicy | |
| self.context.automaticallyMergesChangesFromParent = true | |
| self.context.retainsRegisteredObjects = true | |
| self.context.shouldDeleteInaccessibleFaults = true | |
| self.context.persistentStoreCoordinator = storeCoordinator | |
| } catch let error as StorageError { | |
| throw error | |
| } catch { | |
| throw StorageError.internalError(error) | |
| } | |
| } | |
| fileprivate static func migrateIfNeeded( | |
| _ url: URL, | |
| type: StoreType, | |
| migrations: Array<StorageMigration> | |
| ) throws { | |
| do { | |
| let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: type.coreDataType, at: url, options: nil) | |
| guard var schemaVersion = metadata["SchemaVersion"] as? Int | |
| else { fatalError("Missing SchemaVersion") } | |
| for migration in migrations { | |
| guard migration.version > schemaVersion else { continue } | |
| try migration.migrate(url, type: type, version: schemaVersion) | |
| schemaVersion = migration.version | |
| } | |
| } catch let error as StorageError { | |
| throw error | |
| } catch let error as NSError { | |
| guard error.code == 260, error.domain == "NSCocoaErrorDomain" | |
| else { throw StorageError.internalError(error) } | |
| // ignore missing file error = there is no store yet so no migration is needed | |
| } | |
| } | |
| } | |
| public extension Storage { | |
| func loadAsync<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all, | |
| sorting: StorageSorting? = nil, | |
| limit: Int = 0, | |
| offset: Int = 0, | |
| _ callback: @escaping ResultCallback<Array<Entity>, StorageError> | |
| ) { | |
| queue.async { [weak self] in | |
| guard let self = self else { return } | |
| callback(self.unsafeLoad(type, where: filter, sorting: sorting, limit: limit, offset: offset)) | |
| } | |
| } | |
| func load<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all, | |
| sorting: StorageSorting? = nil, | |
| limit: Int = 0, | |
| offset: Int = 0 | |
| ) -> Result<Array<Entity>, StorageError> { | |
| queue.sync { self.unsafeLoad(type, where: filter, sorting: sorting, limit: limit, offset: offset) } | |
| } | |
| func saveAsync<Entity: StorageEntity>( | |
| _ entity: Entity, | |
| _ callback: @escaping ResultCallback<Void, StorageError> | |
| ) { | |
| queue.async { [weak self] in | |
| guard let self = self else { return } | |
| callback(self.unsafeSave(entity)) | |
| } | |
| } | |
| func save<Entity: StorageEntity>( | |
| _ entity: Entity | |
| ) -> Result<Void, StorageError> { | |
| queue.sync { unsafeSave(entity) } | |
| } | |
| func saveAsync<Entities>( | |
| _ entities: Entities, | |
| _ callback: @escaping ResultCallback<Void, StorageError> | |
| ) where Entities: Collection, Entities.Element: StorageEntity { | |
| queue.async { [weak self] in | |
| guard let self = self else { return } | |
| callback(self.unsafeSave(entities)) | |
| } | |
| } | |
| func save<Entities>( | |
| _ entities: Entities | |
| ) -> Result<Void, StorageError> where Entities: Collection, Entities.Element: StorageEntity { | |
| queue.sync { self.unsafeSave(entities) } | |
| } | |
| func updateAsync<Entity: StorageEntity, Value>( | |
| _ keyPath: WritableKeyPath<Entity, Value>, | |
| to value: Value, | |
| where filter: StorageFilter = .all, | |
| _ callback: @escaping ResultCallback<Void, StorageError> | |
| ) { | |
| queue.async { [weak self] in | |
| guard let self = self else { return } | |
| callback(self.unsafeUpdate(keyPath, to: value, where: filter)) | |
| } | |
| } | |
| func update<Entity: StorageEntity, Value>( | |
| _ keyPath: WritableKeyPath<Entity, Value>, | |
| to value: Value, | |
| where filter: StorageFilter = .all | |
| ) -> Result<Void, StorageError> { | |
| queue.sync { self.unsafeUpdate(keyPath, to: value, where: filter) } | |
| } | |
| func deleteAsync<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all, | |
| _ callback: @escaping ResultCallback<Void, StorageError> | |
| ) { | |
| queue.async { [weak self] in | |
| guard let self = self else { return } | |
| callback(self.unsafeDelete(type, where: filter)) | |
| } | |
| } | |
| func delete<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all | |
| ) -> Result<Void, StorageError> { | |
| queue.sync { self.unsafeDelete(type, where: filter) } | |
| } | |
| } | |
| private extension Storage { | |
| func unsafeLoad<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all, | |
| sorting: StorageSorting? = nil, | |
| limit: Int = 0, | |
| offset: Int = 0 | |
| ) -> Result<Array<Entity>, StorageError> { | |
| dispatchPrecondition(condition: .onQueue(queue)) | |
| do { | |
| return try .success( | |
| context | |
| .fetch( | |
| fetchRequest( | |
| for: Entity.model.name, | |
| predicate: filter.predicate, | |
| sorting: sorting?.descriptors ?? [], | |
| limit: limit, | |
| offset: offset | |
| ) | |
| ) | |
| .compactMap { | |
| guard !$0.isDeleted else { return nil } | |
| return try Entity(from: StorageEntityDecoder(managed: $0, context: context)) | |
| } | |
| ) | |
| } catch let error as StorageError { | |
| return .failure(error) | |
| } catch { | |
| return .failure(.internalError(error)) | |
| } | |
| } | |
| func unsafeSave<Entity: StorageEntity>( | |
| _ entity: Entity | |
| ) -> Result<Void, StorageError> { | |
| dispatchPrecondition(condition: .onQueue(queue)) | |
| do { | |
| let managed = NSManagedObject(entity: type(of: entity).model.entityDescription, insertInto: context) | |
| let encoder = StorageEntityEncoder(managed: managed, context: context) | |
| try entity.encode(to: encoder) | |
| context.insert(managed) | |
| try context.save() | |
| return .success(()) | |
| } catch let error as StorageError { | |
| context.rollback() | |
| return .failure(error) | |
| } catch { | |
| context.rollback() | |
| return .failure(.internalError(error)) | |
| } | |
| } | |
| func unsafeSave<Entities>( | |
| _ entities: Entities | |
| ) -> Result<Void, StorageError> where Entities: Collection, Entities.Element: StorageEntity { | |
| dispatchPrecondition(condition: .onQueue(queue)) | |
| do { | |
| try entities.forEach { entity in | |
| let managed = NSManagedObject(entity: type(of: entity).model.entityDescription, insertInto: context) | |
| let encoder = StorageEntityEncoder(managed: managed, context: context) | |
| try entity.encode(to: encoder) | |
| context.insert(managed) | |
| } | |
| try context.save() | |
| return .success(()) | |
| } catch let error as StorageError { | |
| context.rollback() | |
| return .failure(error) | |
| } catch { | |
| context.rollback() | |
| return .failure(.internalError(error)) | |
| } | |
| } | |
| func unsafeUpdate<Entity: StorageEntity, Value>( | |
| _ keyPath: WritableKeyPath<Entity, Value>, | |
| to value: Value, | |
| where filter: StorageFilter = .all | |
| ) -> Result<Void, StorageError> { | |
| dispatchPrecondition(condition: .onQueue(queue)) | |
| do { | |
| try context | |
| .fetch( | |
| fetchRequest( | |
| for: Entity.model.name, | |
| predicate: filter.predicate | |
| ) | |
| ) | |
| .compactMap { (managed: NSManagedObject) -> Entity? in | |
| guard !managed.isDeleted else { return nil } | |
| defer { context.delete(managed) } | |
| return try Entity(from: StorageEntityDecoder(managed: managed, context: context)) | |
| } | |
| .forEach { (entity: Entity) -> Void in | |
| var entity = entity | |
| entity[keyPath: keyPath] = value | |
| let managed = NSManagedObject(entity: type(of: entity).model.entityDescription, insertInto: context) | |
| let encoder = StorageEntityEncoder(managed: managed, context: context) | |
| try entity.encode(to: encoder) | |
| context.insert(managed) | |
| } | |
| try context.save() | |
| return .success(()) | |
| } catch let error as StorageError { | |
| context.rollback() | |
| return .failure(error) | |
| } catch { | |
| context.rollback() | |
| return .failure(.internalError(error)) | |
| } | |
| } | |
| func unsafeDelete<Entity: StorageEntity>( | |
| _ type: Entity.Type, | |
| where filter: StorageFilter = .all | |
| ) -> Result<Void, StorageError> { | |
| dispatchPrecondition(condition: .onQueue(queue)) | |
| do { | |
| try self.context | |
| .fetch( | |
| fetchRequest( | |
| for: Entity.model.name, | |
| predicate: filter.predicate | |
| ) | |
| ) | |
| .forEach(self.context.delete) | |
| try context.save() | |
| return .success(()) | |
| } catch let error as StorageError { | |
| context.rollback() | |
| return .failure(error) | |
| } catch { | |
| context.rollback() | |
| return .failure(.internalError(error)) | |
| } | |
| } | |
| } | |
| private func fetchRequest( | |
| for name: String, | |
| predicate: NSPredicate? = nil, | |
| sorting: [NSSortDescriptor] = [], | |
| limit: Int = 0, | |
| offset: Int = 0 | |
| ) -> NSFetchRequest<NSManagedObject> { | |
| let request = NSFetchRequest<NSManagedObject>(entityName: name) | |
| request.returnsObjectsAsFaults = false | |
| request.includesPropertyValues = true | |
| request.predicate = predicate | |
| request.sortDescriptors = sorting | |
| request.fetchBatchSize = limit | |
| request.fetchOffset = offset | |
| return request | |
| } | |
| // MARK: - StorageError.swift | |
| public enum StorageError: Error { | |
| case dataDeleted | |
| case dataInconsistency | |
| case internalError(Error) | |
| } | |
| // MARK: - StorageSchema.swift | |
| import CoreData | |
| public struct StorageSchema { | |
| public typealias Version = Int | |
| internal let version: Version | |
| internal let managedObjectModel: NSManagedObjectModel = NSManagedObjectModel() | |
| public init(version: Version, entities: StorageEntity.Type...) { | |
| self.version = version | |
| managedObjectModel.entities = entities.map { $0.model.entityDescription } | |
| } | |
| } | |
| // MARK: - StorageEntity.swift | |
| public protocol StorageEntity: Codable { | |
| static var model: StorageEntityModel { get } | |
| } | |
| // MARK: - StorageEntityModel.swift | |
| import CoreData | |
| public final class StorageEntityModel: Hashable { | |
| public var name: String | |
| public var fields: Set<StorageEntityField> | |
| public init( | |
| name: String, | |
| fields: StorageEntityField... | |
| ) { | |
| self.name = name | |
| self.fields = Set(fields) | |
| } | |
| // you have to keep the same reference to the model through application lifetime | |
| deinit { fatalError("Reference to \(Self.self) lost!") } | |
| internal lazy var entityDescription: NSEntityDescription = { | |
| let entityDescription: NSEntityDescription = .init() | |
| entityDescription.name = name | |
| entityDescription.isAbstract = false | |
| let properties = fields.map(\.propertyDescription) | |
| properties | |
| .compactMap { $0 as? NSRelationshipDescription } | |
| .forEach { | |
| $0.inverseRelationship?.name += "_\(name)" | |
| $0.inverseRelationship?.destinationEntity = entityDescription | |
| $0.inverseRelationship?.inverseRelationship = $0 | |
| } | |
| entityDescription.properties = properties | |
| entityDescription.indexes = fields.compactMap { field in | |
| guard field.isIndexed, let property = properties.first(where: { $0.name == field.name }) else { return nil } | |
| return NSFetchIndexDescription( | |
| name: "\(field.name)_idx", | |
| elements: [ | |
| NSFetchIndexElementDescription( | |
| property: property, | |
| collationType: .binary | |
| ) | |
| ] | |
| ) | |
| } | |
| entityDescription.uniquenessConstraints = fields.compactMap { field in | |
| guard field.isUnique else { return nil } | |
| return [NSString(string: field.name)] | |
| } | |
| return entityDescription | |
| }() | |
| public func hash(into hasher: inout Hasher) { | |
| hasher.combine(name) | |
| hasher.combine(fields) | |
| } | |
| public static func == (lhs: StorageEntityModel, rhs: StorageEntityModel) -> Bool { | |
| lhs.name == rhs.name && lhs.fields == rhs.fields | |
| } | |
| } | |
| // MARK: - StorageEntityField.swift | |
| import CoreData | |
| public final class StorageEntityField: Hashable { | |
| public let name: String | |
| public let type: StorageEntityFieldType | |
| public var isUnique: Bool { type.isUnique } | |
| public var isIndexed: Bool { type.isIndexed } | |
| public init( | |
| key: CodingKey, | |
| type: StorageEntityFieldType | |
| ) { | |
| self.name = key.stringValue | |
| self.type = type | |
| } | |
| internal lazy var propertyDescription: NSPropertyDescription = { | |
| let property: NSPropertyDescription | |
| switch type { | |
| case let .bool(optional, indexed): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .booleanAttributeType | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .string(optional, indexed, _): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .stringAttributeType | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .int(optional, indexed, _): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .integer64AttributeType | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .double(optional, indexed): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .doubleAttributeType | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .array(type, optional, indexed): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .transformableAttributeType | |
| attribute.valueTransformerName = "NSSecureUnarchiveFromDataTransformer" | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .data(optional, indexed, external): | |
| let attribute: NSAttributeDescription = NSAttributeDescription() | |
| attribute.attributeType = .binaryDataAttributeType | |
| attribute.allowsExternalBinaryDataStorage = external | |
| attribute.isOptional = optional | |
| property = attribute | |
| case let .entity(type, optional, indexed): | |
| let relationship: NSRelationshipDescription = NSRelationshipDescription() | |
| relationship.deleteRule = .cascadeDeleteRule | |
| relationship.destinationEntity = type.model.entityDescription | |
| relationship.minCount = optional ? 0 : 1 | |
| relationship.maxCount = 1 | |
| relationship.isOptional = optional | |
| let inverseRelationship: NSRelationshipDescription = NSRelationshipDescription() | |
| inverseRelationship.name = "_inverse_\(name)" | |
| inverseRelationship.deleteRule = optional ? .nullifyDeleteRule : .cascadeDeleteRule | |
| inverseRelationship.minCount = 0 | |
| inverseRelationship.maxCount = 1 | |
| inverseRelationship.isOptional = true | |
| relationship.inverseRelationship = inverseRelationship | |
| type.model.entityDescription.properties.append(inverseRelationship) | |
| property = relationship | |
| case let .list(type, indexed): | |
| let relationship: NSRelationshipDescription = NSRelationshipDescription() | |
| relationship.deleteRule = .nullifyDeleteRule | |
| relationship.destinationEntity = type.model.entityDescription | |
| relationship.minCount = 0 | |
| relationship.maxCount = 0 | |
| relationship.isOptional = true | |
| let inverseRelationship: NSRelationshipDescription = NSRelationshipDescription() | |
| inverseRelationship.name = "_inverse_\(name)" | |
| inverseRelationship.deleteRule = .nullifyDeleteRule | |
| inverseRelationship.minCount = 0 | |
| inverseRelationship.maxCount = 0 | |
| inverseRelationship.isOptional = true | |
| relationship.inverseRelationship = inverseRelationship | |
| type.model.entityDescription.properties.append(inverseRelationship) | |
| property = relationship | |
| } | |
| property.name = name | |
| return property | |
| }() | |
| public func hash(into hasher: inout Hasher) { | |
| hasher.combine(name) | |
| hasher.combine(type) | |
| } | |
| public static func == (lhs: StorageEntityField, rhs: StorageEntityField) -> Bool { | |
| lhs.name == rhs.name && lhs.type == rhs.type | |
| } | |
| } | |
| // MARK: - StorageEntityFieldType.swift | |
| import CoreData | |
| public enum StorageEntityFieldType: Hashable { | |
| case bool(optional: Bool, indexed: Bool) | |
| case string(optional: Bool, indexed: Bool, unique: Bool) | |
| case int(optional: Bool, indexed: Bool, unique: Bool) // TODO: int8/16/32/64 ?? | |
| case double(optional: Bool, indexed: Bool) // TODO: float ?? | |
| /// - warning: it cannot be filtered only saved and loaded | |
| case array(of: (Any & Codable).Type, optional: Bool, indexed: Bool) | |
| // TODO: dictionary of | |
| case data(optional: Bool, indexed: Bool, external: Bool) | |
| case entity(of: StorageEntity.Type, optional: Bool, indexed: Bool) | |
| /// - warning: it cannot be optional - it becomes empty array instead | |
| case list(of: StorageEntity.Type, indexed: Bool) | |
| public var isOptional: Bool { | |
| switch self { | |
| case | |
| let .bool(optional, _), | |
| let .string(optional, _, _), | |
| let .int(optional, _, _), | |
| let .double(optional, _), | |
| let .array(_, optional, _), | |
| let .data(optional, _, _), | |
| let .entity(_, optional, _): | |
| return optional | |
| case .list: | |
| return false // TODO: to verify | |
| } | |
| } | |
| public var isIndexed: Bool { | |
| switch self { | |
| case | |
| let .bool(_, indexed), | |
| let .string(_, indexed, _), | |
| let .int(_, indexed, _), | |
| let .double(_, indexed), | |
| let .array(_, _, indexed), | |
| let .data(_, indexed, _), | |
| let .entity(_, _, indexed), | |
| let .list(_, indexed): | |
| return indexed | |
| } | |
| } | |
| public var isUnique: Bool { | |
| switch self { | |
| case | |
| .bool, | |
| .double, | |
| .array, | |
| .data, | |
| .entity, | |
| .list: | |
| return false | |
| case let .string(_, _, unique), | |
| let .int(_, _, unique): | |
| return unique | |
| } | |
| } | |
| public var isRelation: Bool { | |
| switch self { | |
| case | |
| .bool, | |
| .string, | |
| .int, | |
| .double, | |
| .array, | |
| .data: | |
| return false | |
| case .entity, .list: | |
| return true | |
| } | |
| } | |
| public func hash(into hasher: inout Hasher) { | |
| switch self { | |
| case let .bool(optional, indexed): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine("bool") | |
| case let .string(optional, indexed, unique): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine(unique) | |
| hasher.combine("string") | |
| case let .int(optional, indexed, unique): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine(unique) | |
| hasher.combine("int") | |
| case let .double(optional, indexed): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine("double") | |
| case let .array(type, optional, indexed): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine("\(type)") | |
| hasher.combine("array") | |
| case let .data(optional, indexed, external): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine(external) | |
| hasher.combine("data") | |
| case let .entity(type, optional, indexed): | |
| hasher.combine(optional) | |
| hasher.combine(indexed) | |
| hasher.combine("\(type)") | |
| hasher.combine("entity") | |
| case let .list(type, indexed): | |
| hasher.combine(indexed) | |
| hasher.combine("\(type)") | |
| hasher.combine("list") | |
| } | |
| } | |
| public static func == (lhs: StorageEntityFieldType, rhs: StorageEntityFieldType) -> Bool { | |
| switch (lhs, rhs) { | |
| case let (.bool(lopt, lind), .bool(ropt, rind)): | |
| return lopt == ropt && lind == rind | |
| case let (.string(lopt, lind, luni), .string(ropt, rind, runi)): | |
| return lopt == ropt && lind == rind && luni == runi | |
| case let (.int(lopt, lind, luni), .int(ropt, rind, runi)): | |
| return lopt == ropt && lind == rind && luni == runi | |
| case let (.double(lopt, lind), .double(ropt, rind)): | |
| return lopt == ropt && lind == rind | |
| case let (.array(ltype, lopt, lind), .array(rtype, ropt, rind)): | |
| return lopt == ropt && ltype == rtype && lind == rind | |
| case let (.data(lopt, lind, lext), .data(ropt, rind, rext)): | |
| return lopt == ropt && lind == rind && lext == rext | |
| case let (.entity(ltype, lopt, lind), .entity(rtype, ropt, rind)): | |
| return lopt == ropt && ltype == rtype && lind == rind | |
| case let (.list(ltype, lind), .list(rtype, rind)): | |
| return ltype == rtype && lind == rind | |
| case _: | |
| return false | |
| } | |
| } | |
| } | |
| // MARK: - StorageFilter.swift | |
| import CoreData | |
| public struct StorageFilter { | |
| internal let predicate: NSPredicate | |
| public init(_ predicate: NSPredicate) { | |
| self.predicate = predicate | |
| } | |
| } | |
| public extension StorageFilter { | |
| static var all: StorageFilter { StorageFilter(NSPredicate(value: true)) } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| equal: Value | |
| ) where Subject: StorageEntity, Value: Equatable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| equal: equal | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| equal: Value | |
| ) where Value: Equatable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K == %@", | |
| argumentArray: [keyPathString, equal] | |
| ) | |
| ) | |
| } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| greaterThanOrEqualTo value: Value | |
| ) where Subject: StorageEntity, Value: Comparable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| greaterThanOrEqualTo: value | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| greaterThanOrEqualTo value: Value | |
| ) where Value: Comparable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K >= %@", | |
| argumentArray: [keyPathString, value] | |
| ) | |
| ) | |
| } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| greaterThan value: Value | |
| ) where Subject: StorageEntity, Value: Comparable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| greaterThan: value | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| greaterThan value: Value | |
| ) where Value: Comparable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K > %@", | |
| argumentArray: [keyPathString, value] | |
| ) | |
| ) | |
| } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| lessThanOrEqualTo value: Value | |
| ) where Subject: StorageEntity, Value: Comparable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| lessThanOrEqualTo: value | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| lessThanOrEqualTo value: Value | |
| ) where Value: Comparable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K <= %@", | |
| argumentArray: [keyPathString, value] | |
| ) | |
| ) | |
| } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| lessThan value: Value | |
| ) where Subject: StorageEntity, Value: Comparable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| lessThan: value | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| lessThan value: Value | |
| ) where Value: Comparable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K < %@", | |
| argumentArray: [keyPathString, value] | |
| ) | |
| ) | |
| } | |
| init<Subject>( | |
| _ keyPath: KeyPath<Subject, String>, | |
| contains: String | |
| ) where Subject: StorageEntity { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| contains: contains | |
| ) | |
| } | |
| init( | |
| keyPathString: String, | |
| contains: String | |
| ) { | |
| self.init( | |
| NSPredicate( | |
| format: "%K CONTAINS[cd] %@", | |
| argumentArray: [keyPathString, contains] | |
| ) | |
| ) | |
| } | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| containsAny valueSet: Set<Value> | |
| ) where Subject: StorageEntity, Value: Hashable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| containsAny: valueSet | |
| ) | |
| } | |
| init<Value>( | |
| keyPathString: String, | |
| containsAny valueSet: Set<Value> | |
| ) where Value: Hashable { | |
| self.init( | |
| NSPredicate( | |
| format: "%K IN %@", | |
| argumentArray: [keyPathString, NSSet(set: valueSet)] | |
| ) | |
| ) | |
| } | |
| init<Subject, Element, Elements>( | |
| _ keyPath: KeyPath<Subject, Elements>, | |
| containsElement element: Element | |
| ) where Subject: StorageEntity, Elements: Collection, Elements.Element == Element, Element: Equatable { | |
| guard let keyPathString = keyPath._kvcKeyPathString | |
| else { fatalError("Unsupported keypath, please mark with `@objc` or use keyPathString") } | |
| self.init( | |
| keyPathString: keyPathString, | |
| containsElement: element | |
| ) | |
| } | |
| init<Element>( | |
| keyPathString: String, | |
| containsElement element: Element | |
| ) where Element: Equatable { | |
| self.init( | |
| NSPredicate( | |
| format: "%@ IN %K", | |
| argumentArray: [element, keyPathString] | |
| ) | |
| ) | |
| } | |
| init(compound filters: StorageFilter...) { | |
| self.init(NSCompoundPredicate(andPredicateWithSubpredicates: filters.map(\.predicate))) | |
| } | |
| } | |
| // MARK: - StorageSorting.swift | |
| import Foundation | |
| public struct StorageSorting { | |
| internal let descriptors: [NSSortDescriptor] | |
| internal init(_ descriptors: [NSSortDescriptor]) { | |
| self.descriptors = descriptors | |
| } | |
| } | |
| public extension StorageSorting { | |
| init<Subject, Value>( | |
| _ keyPath: KeyPath<Subject, Value>, | |
| ascending: Bool = true | |
| ) where Subject: StorageEntity, Value: Comparable { | |
| self.init([NSSortDescriptor(keyPath: keyPath, ascending: ascending)]) | |
| } | |
| init( | |
| key: String, | |
| ascending: Bool = true | |
| ) { | |
| self.init([NSSortDescriptor(key: key, ascending: ascending)]) | |
| } | |
| } | |
| // MARK: - StorageMigration.swift | |
| import CoreData | |
| public struct StorageMigration { | |
| public let version: StorageSchema.Version | |
| private let entityMigrations: Array<StorageEntityMigration.Type> | |
| public init( | |
| version: StorageSchema.Version, | |
| entityMigrations: Array<StorageEntityMigration.Type> | |
| ) { | |
| self.version = version | |
| self.entityMigrations = entityMigrations | |
| } | |
| internal func migrate(_ url: URL, type: Storage.StoreType, version currentVersion: StorageSchema.Version) throws { | |
| guard case .sqlite = type else { return } | |
| let tempURL = url.appendingPathExtension("migration") | |
| let fileManager = FileManager.default | |
| if fileManager.fileExists(atPath: tempURL.path) { | |
| try fileManager.removeItem(at: tempURL) | |
| // tempURL + "-wal" and tempURL + "-shm" mitght be also there to remove | |
| } else { /**/ } | |
| // TODO: we might have more than one store - we should allow multiple data models for that | |
| let sourceModel = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(Data.init(contentsOf: url.deletingLastPathComponent().appendingPathComponent("CodeDataModel_\(currentVersion)"))) as! NSManagedObjectModel | |
| let destinationModel = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(Data.init(contentsOf: url.deletingLastPathComponent().appendingPathComponent("CodeDataModel_\(version)"))) as! NSManagedObjectModel | |
| let mappingModel = try NSMappingModel.inferredMappingModel(forSourceModel: sourceModel, destinationModel: destinationModel) | |
| mappingModel.entityMappings = mappingModel.entityMappings! | |
| .map { mapping in | |
| guard let migration = entityMigrations.first(where: { $0.sourceEntityName == mapping.sourceEntityName }) else { return mapping } | |
| return migration.mapping(from: mapping) | |
| } | |
| let migrationManager = NSMigrationManager( | |
| sourceModel: sourceModel, | |
| destinationModel: destinationModel | |
| ) | |
| try migrationManager.migrateStore( | |
| from: url, | |
| sourceType: type.coreDataType, | |
| options: [NSIgnorePersistentStoreVersioningOption: true], | |
| with: mappingModel, | |
| toDestinationURL: tempURL, | |
| destinationType: type.coreDataType, | |
| destinationOptions: [NSIgnorePersistentStoreVersioningOption: true] | |
| ) | |
| try NSPersistentStoreCoordinator.setMetadata(["SchemaVersion" : version], forPersistentStoreOfType: type.coreDataType, at: tempURL, options: nil) | |
| try NSPersistentStoreCoordinator().replacePersistentStore( | |
| at: url, | |
| destinationOptions: nil, | |
| withPersistentStoreFrom: tempURL, | |
| sourceOptions: nil, | |
| ofType: type.coreDataType | |
| ) | |
| } | |
| } | |
| // MARK: - StorageEntityMigration.swift | |
| import CoreData | |
| open class StorageEntityMigration: NSEntityMigrationPolicy { | |
| open class var sourceEntityName: String? { destinationEntityName } | |
| open class var destinationEntityName: String { fatalError("You have to override this property") } | |
| internal class func mapping(from currentMapping: NSEntityMapping) -> NSEntityMapping { | |
| let mapping = NSEntityMapping() | |
| mapping.mappingType = .customEntityMappingType | |
| mapping.name = "\(destinationEntityName)Mapping" | |
| mapping.entityMigrationPolicyClassName = "\(Self.self)" | |
| mapping.sourceEntityName = sourceEntityName | |
| mapping.sourceEntityVersionHash = currentMapping.sourceEntityVersionHash | |
| mapping.destinationEntityName = destinationEntityName | |
| mapping.destinationEntityVersionHash = currentMapping.destinationEntityVersionHash | |
| mapping.sourceExpression = currentMapping.sourceExpression | |
| mapping.attributeMappings = currentMapping.attributeMappings | |
| mapping.relationshipMappings = currentMapping.relationshipMappings | |
| return mapping | |
| } | |
| open func migrate(from entityFrom: StorageDynamicEntity, to entityTo: StorageDynamicEntity) throws -> Bool { | |
| fatalError("You have to override this method") | |
| } | |
| override open func createDestinationInstances( | |
| forSource source: NSManagedObject, | |
| in mapping: NSEntityMapping, | |
| manager: NSMigrationManager | |
| ) throws { | |
| let destination = NSEntityDescription.insertNewObject( | |
| forEntityName: mapping.destinationEntityName!, | |
| into: manager.destinationContext | |
| ) | |
| for attribute in mapping.attributeMappings ?? [] { | |
| guard | |
| let name = attribute.name, | |
| let valueExpression = attribute.valueExpression | |
| else { continue } | |
| let value: Any? = valueExpression.expressionValue( | |
| with: source, | |
| context: ["source": source] | |
| ) | |
| if value is NSNull || value == nil { | |
| destination.setNilValueForKey(name) | |
| } else { | |
| destination.setValue(value, forKey: name) | |
| } | |
| } | |
| guard try migrate( | |
| from: StorageDynamicEntity(source: source), | |
| to: StorageDynamicEntity(source: destination) | |
| ) | |
| else { return manager.destinationContext.delete(destination) } | |
| manager.associate( | |
| sourceInstance: source, | |
| withDestinationInstance: destination, | |
| for: mapping | |
| ) | |
| } | |
| } | |
| // MARK: - StorageDynamicEntity.swift | |
| import Foundation | |
| @dynamicMemberLookup | |
| public final class StorageDynamicEntity { | |
| internal let source: NSObject | |
| internal init(source: NSObject) { | |
| self.source = source | |
| } | |
| public subscript<Literal>(dynamicMember: Literal) -> Bool? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible { | |
| get { (source.value(forKey: String(dynamicMember)) as? NSNumber)?.boolValue } | |
| set { source.setValue(newValue.map(NSNumber.init(value:)), forKey: String(dynamicMember))} | |
| } | |
| public subscript<Literal>(dynamicMember dynamicMember: Literal) -> String? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible { | |
| get { (source.value(forKey: String(dynamicMember)) as? String) } | |
| set { source.setValue(newValue, forKey: String(dynamicMember))} | |
| } | |
| public subscript<Literal>(dynamicMember dynamicMember: Literal) -> Int? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible { | |
| get { (source.value(forKey: String(dynamicMember)) as? NSNumber)?.intValue } | |
| set { source.setValue(newValue.map(NSNumber.init(value:)), forKey: String(dynamicMember))} | |
| } | |
| public subscript<Literal>(dynamicMember dynamicMember: Literal) -> Double? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible { | |
| get { (source.value(forKey: String(dynamicMember)) as? NSNumber)?.doubleValue } | |
| set { source.setValue(newValue.map(NSNumber.init(value:)), forKey: String(dynamicMember))} | |
| } | |
| public subscript<Literal, Element>(dynamicMember dynamicMember: Literal) -> Array<Element>? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible, Element: Codable { | |
| get { (source.value(forKey: String(dynamicMember)) as? NSArray) as? Array<Element> } | |
| set { source.setValue(newValue, forKey: String(dynamicMember))} | |
| } | |
| public subscript<Literal>(dynamicMember dynamicMember: Literal) -> StorageDynamicEntity? | |
| where Literal: ExpressibleByStringLiteral & LosslessStringConvertible { | |
| get { (source.value(forKey: String(dynamicMember)) as? NSObject).map(StorageDynamicEntity.init(source:)) } | |
| set { source.setValue(newValue, forKey: String(dynamicMember))} | |
| } | |
| } | |
| // MARK: - ResultCallback.swift | |
| public typealias ResultCallback<Success, Failure: Error> = (Result<Success, Failure>) -> Void | |
| // MARK: - StorageEntityEncoder.swift | |
| import Foundation | |
| import CoreData | |
| internal final class StorageEntityEncoder: Encoder { | |
| private let managed: NSManagedObject | |
| private let context: NSManagedObjectContext | |
| public var codingPath: [CodingKey] = [] | |
| public var userInfo: [CodingUserInfoKey : Any] { fatalError("Unsupported operation") } | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| self.managed = managed | |
| self.context = context | |
| } | |
| public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey { | |
| let container = CoreDataManagedContainer<Key>(managed: managed, context: context) | |
| container.codingPath = codingPath | |
| return KeyedEncodingContainer(container) | |
| } | |
| public func unkeyedContainer() -> UnkeyedEncodingContainer { | |
| guard !codingPath.isEmpty else { fatalError("Unsupported operation") } | |
| let list = CoreDataManagedListContainer(managed: managed, context: context) | |
| list.codingPath = codingPath | |
| return list | |
| } | |
| public func singleValueContainer() -> SingleValueEncodingContainer { | |
| fatalError("Unsupported operation") | |
| } | |
| internal final class CoreDataManagedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol { | |
| private let managed: NSManagedObject | |
| private let context: NSManagedObjectContext | |
| internal var codingPath: [CodingKey] = [] | |
| internal var allKeys: [Key] { fatalError("Unsupported operation") } | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| self.managed = managed | |
| self.context = context | |
| } | |
| internal func encodeNil(forKey key: Key) throws { | |
| managed.setPrimitiveValue(nil, forKey: key.stringValue) | |
| } | |
| internal func encode(_ value: Bool, forKey key: Key) throws { | |
| managed.setPrimitiveValue(value, forKey: key.stringValue) | |
| } | |
| internal func encode(_ value: String, forKey key: Key) throws { | |
| managed.setPrimitiveValue(value, forKey: key.stringValue) | |
| } | |
| internal func encode(_ value: Double, forKey key: Key) throws { | |
| managed.setPrimitiveValue(value, forKey: key.stringValue) | |
| } | |
| internal func encode(_ value: Float, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int, forKey key: Key) throws { | |
| managed.setPrimitiveValue(value, forKey: key.stringValue) | |
| } | |
| internal func encode(_ value: Int8, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int16, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int32, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int64, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt8, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt16, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt32, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt64, forKey key: Key) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable { | |
| switch value { | |
| case let entity as StorageEntity: | |
| let relation = NSManagedObject(entity: type(of: entity).model.entityDescription, insertInto: context) | |
| let encoder = StorageEntityEncoder(managed: relation, context: context) | |
| try entity.encode(to: encoder) | |
| relation.setValue(managed, forKey: "_inverse_\(key.stringValue)_\(managed.entity.name!)") | |
| managed.setValue(relation, forKey: key.stringValue) | |
| case let string as String: | |
| managed.setPrimitiveValue(string, forKey: key.stringValue) | |
| case let int as Int: | |
| managed.setPrimitiveValue(int, forKey: key.stringValue) | |
| case let double as Double: | |
| managed.setPrimitiveValue(double, forKey: key.stringValue) | |
| case let bool as Bool: | |
| managed.setPrimitiveValue(bool, forKey: key.stringValue) | |
| case let _ as Array<Any & StorageEntity>: | |
| let encoder = StorageEntityEncoder(managed: managed, context: context) | |
| encoder.codingPath = codingPath + [key] | |
| try value.encode(to: encoder) | |
| case let array as Array<Any>: | |
| managed.setValue(array, forKey: key.stringValue) | |
| case let data as Data: | |
| managed.setValue(data, forKey: key.stringValue) | |
| case nil: | |
| managed.setNilValueForKey(key.stringValue) | |
| case _: | |
| fatalError("Unsupported operation") | |
| } | |
| } | |
| internal func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey { | |
| guard let entity = managed.entity.relationshipsByName[key.stringValue]?.entity else { fatalError("Unsupported operation") } | |
| let relation = NSManagedObject(entity: entity, insertInto: context) | |
| managed.setValue(relation, forKey: key.stringValue) | |
| relation.setValue(managed, forKey: "_inverse_\(key.stringValue)_\(managed.entity.name!)") | |
| let container = CoreDataManagedContainer<NestedKey>(managed: relation, context: context) | |
| container.codingPath = codingPath + [key] | |
| return KeyedEncodingContainer(container) | |
| } | |
| internal func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { | |
| let list = CoreDataManagedListContainer(managed: managed, context: context) | |
| list.codingPath = codingPath + [key] | |
| return list | |
| } | |
| internal func superEncoder() -> Encoder { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func superEncoder(forKey key: Key) -> Encoder { | |
| fatalError("Unsupported operation") | |
| } | |
| } | |
| internal final class CoreDataManagedListContainer: UnkeyedEncodingContainer { | |
| internal var codingPath: [CodingKey] = [] | |
| internal var count: Int { managed.mutableSetValue(forKey: stringKey).count } | |
| internal var stringKey: String { codingPath.last!.stringValue } | |
| private let managed: NSManagedObject | |
| private let context: NSManagedObjectContext | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| self.managed = managed | |
| self.context = context | |
| } | |
| internal func encode<T>(_ value: T) throws where T : Encodable { | |
| guard let entity = value as? StorageEntity else { throw StorageError.dataInconsistency } | |
| let relation = NSManagedObject(entity: type(of: entity).model.entityDescription, insertInto: context) | |
| let encoder = StorageEntityEncoder(managed: relation, context: context) | |
| try entity.encode(to: encoder) | |
| managed.mutableSetValue(forKey: stringKey).add(relation) | |
| relation.mutableSetValue(forKey: "_inverse_\(stringKey)_\(managed.entity.name!)").add(managed) | |
| } | |
| internal func encodeNil() throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Bool) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: String) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Double) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Float) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int8) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int16) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int32) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: Int64) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt8) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt16) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt32) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func encode(_ value: UInt64) throws { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey { | |
| guard let entity = managed.entity.relationshipsByName[stringKey]?.entity else { fatalError("Unsupported operation") } | |
| let relation = NSManagedObject(entity: entity, insertInto: context) | |
| managed.mutableSetValue(forKey: stringKey).add(relation) | |
| relation.mutableSetValue(forKey: "_inverse_\(stringKey)_\(managed.entity.name!)").add(managed) | |
| let container = CoreDataManagedContainer<NestedKey>(managed: relation, context: context) | |
| container.codingPath = codingPath | |
| return KeyedEncodingContainer(container) | |
| } | |
| internal func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func superEncoder() -> Encoder { | |
| fatalError("Unsupported operation") | |
| } | |
| } | |
| } | |
| // MARK: - StorageEntityDecoder.swift | |
| import Foundation | |
| import CoreData | |
| internal final class StorageEntityDecoder: Decoder { | |
| private let managed: NSManagedObject | |
| private let context: NSManagedObjectContext | |
| public var codingPath: [CodingKey] = [] | |
| public var userInfo: [CodingUserInfoKey : Any] { fatalError("Unsupported operation") } | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| assert(!managed.isDeleted) | |
| self.managed = managed | |
| self.context = context | |
| } | |
| public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey { | |
| let container = CoreDataManagedContainer<Key>(managed: managed, context: context) | |
| container.codingPath = codingPath | |
| return KeyedDecodingContainer(container) | |
| } | |
| public func unkeyedContainer() throws -> UnkeyedDecodingContainer { | |
| guard !codingPath.isEmpty else { fatalError("Unsupported operation") } | |
| let container = CoreDataManagedListContainer(managed: managed, context: context) | |
| container.codingPath = codingPath | |
| return container | |
| } | |
| public func singleValueContainer() throws -> SingleValueDecodingContainer { | |
| fatalError("Unsupported operation") | |
| } | |
| internal final class CoreDataManagedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol { | |
| private let managed: NSManagedObject | |
| private let context: NSManagedObjectContext | |
| internal var codingPath: [CodingKey] = [] | |
| internal var allKeys: [Key] { fatalError("Unsupported operation") } | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| self.managed = managed | |
| self.context = context | |
| } | |
| internal func contains(_ key: Key) -> Bool { | |
| managed.entity.attributesByName.keys.contains(key.stringValue) | |
| || managed.entity.relationshipsByName.keys.contains(key.stringValue) | |
| } | |
| internal func decodeNil(forKey key: Key) throws -> Bool { | |
| managed.primitiveValue(forKey: key.stringValue) == nil | |
| || managed.value(forKey: key.stringValue) == nil | |
| } | |
| internal func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { | |
| guard let value = managed.primitiveValue(forKey: key.stringValue) as? Bool | |
| else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: String.Type, forKey key: Key) throws -> String { | |
| guard let value = managed.primitiveValue(forKey: key.stringValue) as? String | |
| else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Double.Type, forKey key: Key) throws -> Double { | |
| guard let value = managed.primitiveValue(forKey: key.stringValue) as? Double | |
| else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Float.Type, forKey key: Key) throws -> Float { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int.Type, forKey key: Key) throws -> Int { | |
| guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int | |
| else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { | |
| switch managed.value(forKey: key.stringValue) { | |
| case let relation as NSManagedObject: | |
| guard !relation.isDeleted else { throw StorageError.dataDeleted } | |
| let decoder = StorageEntityDecoder(managed: relation, context: context) | |
| decoder.codingPath = codingPath + [key] | |
| return try T(from: decoder) | |
| case let value as T: | |
| return value | |
| case _: | |
| let decoder = StorageEntityDecoder(managed: managed, context: context) | |
| decoder.codingPath = codingPath + [key] | |
| return try T.init(from: decoder) | |
| } | |
| } | |
| internal func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { | |
| guard let relation = managed.value(forKey: key.stringValue) as? NSManagedObject, !relation.isDeleted else { throw StorageError.dataInconsistency } | |
| let container = CoreDataManagedContainer<NestedKey>(managed: relation, context: context) | |
| container.codingPath = codingPath + [key] | |
| return KeyedDecodingContainer(container) | |
| } | |
| internal func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { | |
| let listContainer = CoreDataManagedListContainer(managed: managed, context: context) | |
| listContainer.codingPath = codingPath + [key] | |
| return listContainer | |
| } | |
| internal func superDecoder() throws -> Decoder { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func superDecoder(forKey key: Key) throws -> Decoder { | |
| fatalError("Unsupported operation") | |
| } | |
| } | |
| internal final class CoreDataManagedListContainer: UnkeyedDecodingContainer { | |
| internal var codingPath: [CodingKey] = [] | |
| internal var count: Int? { elements.count } | |
| internal var isAtEnd: Bool { count.map { currentIndex >= $0 } ?? true } | |
| internal var currentIndex: Int = 0 | |
| private let managed: NSManagedObject | |
| private lazy var elements: Array<Any> = { | |
| guard let key = codingPath.last else { fatalError("Unsupported operation") } | |
| let value = managed.value(forKey: key.stringValue) | |
| if let array = value as? NSArray { | |
| return Array<Any>(array) | |
| } else if let set = value as? NSSet { | |
| return Array(set) | |
| } else if value == nil { | |
| return Array() | |
| } else { | |
| fatalError("Unsupported operation") | |
| } | |
| }() | |
| private let context: NSManagedObjectContext | |
| internal init(managed: NSManagedObject, context: NSManagedObjectContext) { | |
| assert(!managed.isDeleted) | |
| self.managed = managed | |
| self.context = context | |
| } | |
| internal func decodeNil() throws -> Bool { | |
| fatalError("Unsupported operation") // ???? | |
| } | |
| internal func decode(_ type: Bool.Type) throws -> Bool { | |
| guard let value = elements[currentIndex] as? Bool else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: String.Type) throws -> String { | |
| guard let value = elements[currentIndex] as? String else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Double.Type) throws -> Double { | |
| guard let value = elements[currentIndex] as? Double else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Float.Type) throws -> Float { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int.Type) throws -> Int { | |
| guard let value = elements[currentIndex] as? Int else { throw StorageError.dataInconsistency } | |
| return value | |
| } | |
| internal func decode(_ type: Int8.Type) throws -> Int8 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int16.Type) throws -> Int16 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int32.Type) throws -> Int32 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: Int64.Type) throws -> Int64 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt.Type) throws -> UInt { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt8.Type) throws -> UInt8 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt16.Type) throws -> UInt16 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt32.Type) throws -> UInt32 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode(_ type: UInt64.Type) throws -> UInt64 { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func decode<T>(_ type: T.Type) throws -> T where T : Decodable { | |
| guard type is StorageEntity.Type else { fatalError("Unsupported operation") } | |
| guard let relation = elements[currentIndex] as? NSManagedObject else { throw StorageError.dataInconsistency } | |
| currentIndex += 1 | |
| guard !relation.isDeleted else { throw StorageError.dataDeleted } | |
| let decoder = StorageEntityDecoder(managed: relation, context: context) | |
| decoder.codingPath = codingPath | |
| return try T(from: decoder) | |
| } | |
| internal func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { | |
| guard let relation = elements[currentIndex] as? NSManagedObject else { throw StorageError.dataInconsistency } | |
| currentIndex += 1 | |
| guard !relation.isDeleted else { throw StorageError.dataDeleted } | |
| let container = CoreDataManagedContainer<NestedKey>(managed: relation, context: context) | |
| container.codingPath = codingPath | |
| return KeyedDecodingContainer(container) | |
| } | |
| internal func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { | |
| fatalError("Unsupported operation") | |
| } | |
| internal func superDecoder() throws -> Decoder { | |
| fatalError("Unsupported operation") | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example
sample models:
store setup:
load/save/delete