Skip to content

Instantly share code, notes, and snippets.

@kaqu
Created September 7, 2019 17:47
Show Gist options
  • Save kaqu/719906b3df34accee97fbe28bf10fe21 to your computer and use it in GitHub Desktop.
Save kaqu/719906b3df34accee97fbe28bf10fe21 to your computer and use it in GitHub Desktop.
CodeData - CoreData from Code
import Foundation
import CoreData
// TODO: FIXME: minimal, naive implementation
internal final class EntityDecoder<EntityType: StorageEntity>: Decoder {
private let managed: NSManagedObject
public var codingPath: [CodingKey] = []
public var userInfo: [CodingUserInfoKey : Any] = [:]
internal init(managed: NSManagedObject) {
self.managed = managed
}
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(CoreDataManagedContainer(managed: managed))
}
public func unkeyedContainer() throws -> UnkeyedDecodingContainer { fatalError("Not supported") }
public func singleValueContainer() throws -> SingleValueDecodingContainer { fatalError("Not supported") }
internal final class CoreDataManagedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
private let managed: NSManagedObject
var codingPath: [CodingKey] = []
var allKeys: [Key] = []
internal init(managed: NSManagedObject) {
self.managed = managed
}
func contains(_ key: Key) -> Bool {
managed.entity.attributesByName.keys.contains(key.stringValue)
}
func decodeNil(forKey key: Key) throws -> Bool {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Bool else { fatalError("THROW") }
return value
}
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
fatalError()
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? String else { fatalError("THROW") }
return value
}
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Double else { fatalError("THROW") }
return value
}
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Float else { fatalError("THROW") }
return value
}
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int else { fatalError("THROW") }
return value
}
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int8 else { fatalError("THROW") }
return value
}
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int16 else { fatalError("THROW") }
return value
}
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int32 else { fatalError("THROW") }
return value
}
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? Int64 else { fatalError("THROW") }
return value
}
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? UInt else { fatalError("THROW") }
return value
}
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? UInt8 else { fatalError("THROW") }
return value
}
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? UInt16 else { fatalError("THROW") }
return value
}
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? UInt32 else { fatalError("THROW") }
return value
}
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
guard let value = managed.primitiveValue(forKey: key.stringValue) as? UInt64 else { fatalError("THROW") }
return value
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
fatalError()
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
fatalError() // ???
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
fatalError()
}
func superDecoder() throws -> Decoder {
fatalError()
}
func superDecoder(forKey key: Key) throws -> Decoder {
fatalError()
}
}
}
import Foundation
import CoreData
// TODO: FIXME: minimal, naive implementation
internal final class EntityEncoder: Encoder {
private let managed: NSManagedObject
public var codingPath: [CodingKey] = []
public var userInfo: [CodingUserInfoKey : Any] = [:]
internal init(managed: NSManagedObject) {
self.managed = managed
}
public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
return KeyedEncodingContainer(CoreDataManagedContainer(managed: managed))
}
public func unkeyedContainer() -> UnkeyedEncodingContainer {
fatalError()
}
public func singleValueContainer() -> SingleValueEncodingContainer {
fatalError()
}
internal final class CoreDataManagedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
private let managed: NSManagedObject
var codingPath: [CodingKey] = []
var allKeys: [Key] = []
internal init(managed: NSManagedObject) {
self.managed = managed
}
func encodeNil(forKey key: Key) throws {
managed.setPrimitiveValue(nil, forKey: key.stringValue)
}
func encode(_ value: Bool, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: String, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Double, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Float, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Int, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Int8, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Int16, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Int32, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: Int64, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: UInt, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: UInt8, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: UInt16, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: UInt32, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode(_ value: UInt64, forKey key: Key) throws {
managed.setPrimitiveValue(value, forKey: key.stringValue)
}
func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
fatalError()
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
fatalError()
}
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
fatalError()
}
func superEncoder() -> Encoder {
fatalError()
}
func superEncoder(forKey key: Key) -> Encoder {
fatalError()
}
}
}
internal protocol OptionalType {
static var wrappedType: Any.Type { get }
}
extension Optional : OptionalType {
static var wrappedType: Any.Type { return Wrapped.self }
}
import CoreData
public final class Storage {
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 = "CoreDataStorage", store: StoreType = .inMemory, schema: StorageSchema) throws {
self.schema = schema
let storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: schema.managedObjectModel)
try storeCoordinator
.addPersistentStore(
ofType: store.coreDataType,
configurationName: nil,
at: NSPersistentContainer.defaultDirectoryURL().appendingPathComponent(name),
options: nil//NSMigratePersistentStoresAutomaticallyOption
)
context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = storeCoordinator
}
}
public extension Storage { // FIXME: only for quick test
func load<Entity: StorageEntity>(_ type: Entity.Type) throws -> [Entity] {
return try context.fetch(NSFetchRequest<NSManagedObject>.init(entityName: Entity.entityName))
.map {
var entity = try Entity(from: EntityDecoder<AnotherModel>(managed: $0))
entity.managedID = $0.objectID
return entity
}
}
func save<Entity: StorageEntity>(_ entity: inout Entity) throws {
if let managedID = entity.managedID {
let managed = context.object(with: managedID)
let encoder = CoreDataEncoder(managed: managed)
try entity.encode(to: encoder)
try context.save()
} else {
let managed = NSManagedObject(entity: schema.managedObjectModel.entitiesByName[type(of: entity).entityName]!, insertInto: context)
let encoder = CoreDataEncoder(managed: managed)
try entity.encode(to: encoder)
context.insert(managed)
try context.save()
entity.managedID = managed.objectID
}
}
}
import CoreData
public protocol AnyStorageEntity: Codable {
typealias ManagedID = NSManagedObjectID
static var entityName: String { get }
static func entityDescription(within entities: [String: NSEntityDescription]) -> NSEntityDescription
var managedID: ManagedID? { get set }
}
public extension AnyStorageEntity {
static var entityName: String {
return .init(describing: self)
}
}
public protocol StorageEntity: AnyStorageEntity {
associatedtype CodingKeys: StorageCodingKeys
}
public extension StorageEntity {
static func entityDescription(within entities: [String: NSEntityDescription]) -> NSEntityDescription {
let description: NSEntityDescription = .init()
description.name = Self.entityName
Self.CodingKeys.allCases.forEach { property in
let propertyName: String = property.stringValue
let propertyType: Any.Type
let isOptional: Bool
if let optionalType = property.propertyType as? OptionalType.Type {
propertyType = optionalType.wrappedType
isOptional = true
assert(!(propertyType is OptionalType.Type))
} else {
propertyType = property.propertyType
isOptional = false
}
if propertyType is AnyStorageEntity { // TODO: FIXME: relatons
// let relationship: NSRelationshipDescription = .init()
// relationship.name = propertyName
// description.properties.append(relationship)
fatalError("Not supported yet")
} else { // TODO: FIXME: arrays / collections
let attribute: NSAttributeDescription = .init()
attribute.name = propertyName
attribute.attributeType = coreDataType(for: propertyType)
attribute.isOptional = isOptional
description.properties.append(attribute)
}
}
return description
}
}
public protocol StorageCodingKeys: CodingKey, CaseIterable {
var propertyType: Any.Type { get }
}
private func coreDataType(for type: Any.Type) -> NSAttributeType {
if Int16.self == type { return .integer16AttributeType }
else if Int32.self == type { return .integer32AttributeType }
else if Int64.self == type { return .integer64AttributeType }
else if Int.self == type { return .integer64AttributeType }
else if Decimal.self == type { return .decimalAttributeType }
else if Double.self == type { return .doubleAttributeType }
else if Float.self == type { return .floatAttributeType }
else if String.self == type { return .stringAttributeType }
else if Bool.self == type { return .booleanAttributeType }
else if Date.self == type { return .dateAttributeType }
else if Data.self == type { return .binaryDataAttributeType }
else { fatalError("Unsupported type: \(type)") }
}
import CoreData
public struct StorageSchema {
internal let managedObjectModel: NSManagedObjectModel = .init()
public init(_ entityList: AnyStorageEntity.Type...) {
managedObjectModel.entities.reserveCapacity(entityList.count)
entityList.forEach {
managedObjectModel.entities.append($0.entityDescription(within: managedObjectModel.entitiesByName))
}
}
}
@kaqu
Copy link
Author

kaqu commented Sep 7, 2019

Usage example (for current, naive implementation):

// sample entity
struct Sample: StorageEntity {
  enum CodingKeys: StorageCodingKeys {
    case field
    
    var propertyType: Any.Type {
      switch self {
      case .field:
        return String.self
      }
    }
  }
  
  var managedID: ManagedID? = nil
  var field: String
}
// store with schema
let store: Storage = try .init(
  store: .inMemory, // or sqlite
  schema: StorageSchema(
    Sample.self // just enumerate all used entity types
  )
)
// load all (in current naive POC)
try store.load(Sample.self) // [Sample]
// save / update (in current naive POC)
var toStore: Sample = .init(field: "TEST")
try store.save(&toStore)
toStore.field = "Changed"
// this will update previous entity since it have managedID set after previous save
try store.save(&toStore)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment