Field.Coded implementations for transformable attributes

This commit is contained in:
John Estropia
2020-01-18 16:22:06 +09:00
parent 43f61359da
commit bcc2d9def3
22 changed files with 1505 additions and 101 deletions

View File

@@ -209,20 +209,23 @@ public final class CoreStoreSchema: DynamicSchema {
let rawModel = NSManagedObjectModel()
var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:]
var allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:]
var allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:]
for entity in self.allEntities {
let (entityDescription, customGetterSetterByKeyPaths) = self.entityDescription(
let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = self.entityDescription(
for: entity,
initializer: CoreStoreSchema.firstPassCreateEntityDescription(from:in:)
)
entityDescriptionsByEntity[entity] = (entityDescription.copy() as! NSEntityDescription)
allCustomGettersSetters[entity] = customGetterSetterByKeyPaths
allFieldCoders[entity] = fieldCoders
}
CoreStoreSchema.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity)
CoreStoreSchema.thirdPassConnectInheritanceTreeAndIndexes(for: entityDescriptionsByEntity)
CoreStoreSchema.fourthPassSynthesizeManagedObjectClasses(
for: entityDescriptionsByEntity,
allCustomGettersSetters: allCustomGettersSetters
allCustomGettersSetters: allCustomGettersSetters,
allFieldCoders: allFieldCoders
)
rawModel.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! })
@@ -254,22 +257,46 @@ public final class CoreStoreSchema: DynamicSchema {
private var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:]
private var customGettersSettersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:]
private var fieldCodersByEntity: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:]
private weak var cachedRawModel: NSManagedObjectModel?
private func entityDescription(for entity: DynamicEntity, initializer: (DynamicEntity, ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter])) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) {
private func entityDescription(
for entity: DynamicEntity,
initializer: (DynamicEntity, ModelVersion) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
)
) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
) {
if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] {
return (cachedEntityDescription, self.customGettersSettersByEntity[entity] ?? [:])
return (
cachedEntityDescription,
self.customGettersSettersByEntity[entity] ?? [:],
self.fieldCodersByEntity[entity] ?? [:]
)
}
let modelVersion = self.modelVersion
let (entityDescription, customGetterSetterByKeyPaths) = withoutActuallyEscaping(initializer, do: { $0(entity, modelVersion) })
let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = withoutActuallyEscaping(
initializer,
do: { $0(entity, modelVersion) }
)
self.entityDescriptionsByEntity[entity] = entityDescription
self.customGettersSettersByEntity[entity] = customGetterSetterByKeyPaths
return (entityDescription, customGetterSetterByKeyPaths)
self.fieldCodersByEntity[entity] = fieldCoders
return (entityDescription, customGetterSetterByKeyPaths, fieldCoders)
}
private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) {
private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
) {
let entityDescription = NSEntityDescription()
entityDescription.coreStoreEntity = entity
@@ -280,6 +307,7 @@ public final class CoreStoreSchema: DynamicSchema {
var keyPathsByAffectedKeyPaths: [KeyPathString: Set<KeyPathString>] = [:]
var customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter] = [:]
var fieldCoders: [KeyPathString: Internals.AnyFieldCoder] = [:]
func createProperties(for type: CoreStoreObject.Type) -> [NSPropertyDescription] {
var propertyDescriptions: [NSPropertyDescription] = []
@@ -302,9 +330,15 @@ public final class CoreStoreSchema: DynamicSchema {
description.allowsExternalBinaryDataStorage = entityDescriptionValues.allowsExternalBinaryDataStorage
description.versionHashModifier = entityDescriptionValues.versionHashModifier
description.renamingIdentifier = entityDescriptionValues.renamingIdentifier
let valueTransformer = entityDescriptionValues.valueTransformer
description.valueTransformerName = valueTransformer?.transformerName.rawValue
propertyDescriptions.append(description)
keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths
customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter)
fieldCoders[attribute.keyPath] = valueTransformer
case let attribute as AttributeProtocol:
Internals.assert(
@@ -350,7 +384,7 @@ public final class CoreStoreSchema: DynamicSchema {
}
entityDescription.properties = createProperties(for: entity.type as! CoreStoreObject.Type)
entityDescription.keyPathsByAffectedKeyPaths = keyPathsByAffectedKeyPaths
return (entityDescription, customGetterSetterByKeyPaths)
return (entityDescription, customGetterSetterByKeyPaths, fieldCoders)
}
private static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription]) {
@@ -523,7 +557,11 @@ public final class CoreStoreSchema: DynamicSchema {
}
}
private static func fourthPassSynthesizeManagedObjectClasses(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription], allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]]) {
private static func fourthPassSynthesizeManagedObjectClasses(
for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription],
allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]],
allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]]
) {
func createManagedObjectSubclass(for entityDescription: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]?) {
@@ -636,5 +674,13 @@ public final class CoreStoreSchema: DynamicSchema {
customGetterSetterByKeyPaths: allCustomGettersSetters[dynamicEntity]
)
}
_ = allFieldCoders
.flatMap({ (_, values) in values })
.reduce(
into: [:] as [NSValueTransformerName: Internals.AnyFieldCoder],
{ (result, element) in result[element.value.transformerName] = element.value }
)
.forEach({ (_, fieldCoder) in fieldCoder.register() })
}
}

View File

@@ -177,6 +177,9 @@ extension DynamicSchema {
defaultString = ", initial: /* required */"
}
case .undefinedAttributeType:
#warning("TODO: Field.Computed")
continue
default:
fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)")
}

532
Sources/Field.Coded.swift Normal file
View File

@@ -0,0 +1,532 @@
//
// Field.Coded.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - FieldContainer
extension FieldContainer {
// MARK: - Coded
/**
The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported.
```
class Animal: CoreStoreObject {
@Field.Stored("species")
var species = ""
@Field.Computed("pluralName", customGetter: Animal.pluralName(_:))
var pluralName: String = ""
@Field.PlistCoded("color")
var color: UIColor?
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored.
*/
@propertyWrapper
public struct Coded<V>: AttributeKeyPathStringConvertible, FieldAttributeProtocol {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("name")
var name: String = ""
@Field.Computed("displayName", customGetter: Person.getName(_:))
var displayName: String = ""
private static func getName(_ partialObject: PartialObject<Person>) -> String {
let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName)
if !cachedDisplayName.isEmpty {
return cachedDisplayName
}
let title = partialObject.value(for: \.$title)
let name = partialObject.value(for: \.$name)
let displayName = "\(title) \(name)"
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
return displayName
}
}
```
- parameter initial: the initial value for the property when the object is first create
- parameter keyPath: the permanent attribute name for this property.
- parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.)
- parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
*/
public init<Coder: FieldCoderType>(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder fieldCoderType: Coder.Type,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) where Coder.FieldStoredValue == V {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(fieldCoderType) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: (encode: (V) -> Data?, decode: (Data?) -> V),
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
// MARK: @propertyWrapper
@available(*, unavailable)
public var wrappedValue: V {
get { fatalError() }
set { fatalError() }
}
public var projectedValue: Self {
return self
}
public static subscript(
_enclosingInstance instance: O,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<O, V>,
storage storageKeyPath: ReferenceWritableKeyPath<O, Self>
) -> V {
get {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V
}
set {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue)
}
}
// MARK: AnyKeyPathStringConvertible
public var cs_keyPathString: String {
return self.keyPath
}
// MARK: KeyPathStringConvertible
public typealias ObjectType = O
public typealias DestinationValueType = V
// MARK: AttributeKeyPathStringConvertible
public typealias ReturnValueType = DestinationValueType
// MARK: PropertyProtocol
internal let keyPath: KeyPathString
// MARK: FieldProtocol
internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = field as! Self
if let customGetter = field.customGetter {
return customGetter(PartialObject<O>(rawObject))
}
let keyPath = field.keyPath
switch rawObject.value(forKey: keyPath) {
case let rawValue as V:
return rawValue
default:
return nil
}
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
Internals.assert(
rawObject.isEditableInContext() == true,
"Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction."
)
let newValue = newValue as! V
let field = field as! Self
let keyPath = field.keyPath
if let customSetter = field.customSetter {
return customSetter(PartialObject<O>(rawObject), newValue)
}
return rawObject.setValue(newValue, forKey: keyPath)
}
// MARK: FieldAttributeProtocol
internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues
internal var getter: CoreStoreManagedObject.CustomGetter? {
let keyPath = self.keyPath
guard let customGetter = self.customGetter else {
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
switch rawObject.primitiveValue(forKey: keyPath) {
case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
rawObject.setPrimitiveValue(valueBox.value, forKey: keyPath)
return valueBox.value
case let value:
return value
}
}
}
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
let value = customGetter(PartialObject<O>(rawObject))
return value
}
}
internal var setter: CoreStoreManagedObject.CustomSetter? {
guard let customSetter = self.customSetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any, _ newValue: Any?) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.willChangeValue(forKey: keyPath)
defer {
rawObject.didChangeValue(forKey: keyPath)
}
customSetter(
PartialObject<O>(rawObject),
newValue as! V
)
}
}
// MARK: FilePrivate
fileprivate init(
defaultValue: @escaping () -> Any?,
keyPath: KeyPathString,
isOptional: Bool,
versionHashModifier: @escaping () -> String?,
renamingIdentifier: @escaping () -> String?,
valueTransformer: @escaping () -> Internals.AnyFieldCoder?,
customGetter: ((_ partialObject: PartialObject<O>) -> V)?,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? ,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
self.keyPath = keyPath
self.entityDescriptionValues = {
let fieldCoder = valueTransformer()
return (
attributeType: .transformableAttributeType,
isOptional: isOptional,
isTransient: false,
allowsExternalBinaryDataStorage: false,
versionHashModifier: versionHashModifier(),
renamingIdentifier: renamingIdentifier(),
valueTransformer: fieldCoder,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: Internals.AnyFieldCoder.TransformableDefaultValueCodingBox(
defaultValue: defaultValue(),
fieldCoder: fieldCoder
)
)
}
self.customGetter = customGetter
self.customSetter = customSetter
}
// MARK: Private
private let customGetter: ((_ partialObject: PartialObject<O>) -> V)?
private let customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)?
}
}
// MARK: - FieldContainer.Coded where V: FieldOptionalType
extension FieldContainer.Coded where V: FieldOptionalType {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("name")
var name: String = ""
@Field.Computed("displayName", customGetter: Person.getName(_:))
var displayName: String = ""
private static func getName(_ partialObject: PartialObject<Person>) -> String {
let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName)
if !cachedDisplayName.isEmpty {
return cachedDisplayName
}
let title = partialObject.value(for: \.$title)
let name = partialObject.value(for: \.$name)
let displayName = "\(title) \(name)"
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
return displayName
}
}
```
- parameter initial: the initial value for the property when the object is first create
- parameter keyPath: the permanent attribute name for this property.
- parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.)
- parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
*/
public init<Coder: FieldCoderType>(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: Coder.Type,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) where Coder.FieldStoredValue == V.Wrapped {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(coder) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: (encode: (V) -> Data?, decode: (Data?) -> V),
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
}
// MARK: - FieldContainer.Coded where V: DefaultNSSecureCodable
extension FieldContainer.Coded where V: DefaultNSSecureCodable {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("name")
var name: String = ""
@Field.Computed("displayName", customGetter: Person.getName(_:))
var displayName: String = ""
private static func getName(_ partialObject: PartialObject<Person>) -> String {
let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName)
if !cachedDisplayName.isEmpty {
return cachedDisplayName
}
let title = partialObject.value(for: \.$title)
let name = partialObject.value(for: \.$name)
let displayName = "\(title) \(name)"
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
return displayName
}
}
```
- parameter initial: the initial value for the property when the object is first create
- parameter keyPath: the permanent attribute name for this property.
- parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.)
- parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
*/
public init(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V>.self) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
}
// MARK: - FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable
extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable {
public init(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V.Wrapped>.self) },
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
}

View File

@@ -79,14 +79,12 @@ extension FieldContainer {
}
```
- parameter keyPath: the permanent attribute name for this property.
- parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
*/
public init(
_ keyPath: KeyPathString,
renamingIdentifier: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
@@ -94,7 +92,6 @@ extension FieldContainer {
self.init(
keyPath: keyPath,
isOptional: false,
renamingIdentifier: renamingIdentifier,
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
@@ -102,14 +99,13 @@ extension FieldContainer {
}
/**
Overload for compiler message only
Overload for compiler error message only
*/
@available(*, unavailable, message: "Field.Computed properties are not allowed to have default values.")
@available(*, unavailable, message: "Field.Computed properties are not allowed to have initial values, including `nil`.")
public init(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
renamingIdentifier: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
@@ -274,7 +270,6 @@ extension FieldContainer {
fileprivate init(
keyPath: KeyPathString,
isOptional: Bool,
renamingIdentifier: @escaping () -> String?,
customGetter: ((_ partialObject: PartialObject<O>) -> V)?,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? ,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
@@ -287,7 +282,8 @@ extension FieldContainer {
isTransient: true,
allowsExternalBinaryDataStorage: false,
versionHashModifier: nil,
renamingIdentifier: renamingIdentifier(),
renamingIdentifier: nil,
valueTransformer: nil,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: nil
)

View File

@@ -1,9 +0,0 @@
//
// Field.PlistCoded.swift
// CoreStore
//
// Created by John Estropia on 2020/01/15.
// Copyright © 2020 John Rommel Estropia. All rights reserved.
//
import Foundation

View File

@@ -81,7 +81,7 @@ extension FieldContainer {
- parameter initial: the initial value for the property when the object is first create
- parameter keyPath: the permanent attribute name for this property.
- parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.)
- parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name.
- parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
@@ -90,17 +90,18 @@ extension FieldContainer {
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
renamingIdentifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
wrappedValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: renamingIdentifier,
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
@@ -185,11 +186,14 @@ extension FieldContainer {
return customGetter(PartialObject<O>(rawObject))
}
let keyPath = field.keyPath
guard case let rawValue as V.FieldStoredNativeType = rawObject.value(forKey: keyPath) else {
switch rawObject.value(forKey: keyPath) {
case let rawValue as V.FieldStoredNativeType:
return V.cs_fromFieldStoredNativeType(rawValue)
default:
return nil
}
return V.cs_fromFieldStoredNativeType(rawValue)
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
@@ -284,6 +288,7 @@ extension FieldContainer {
allowsExternalBinaryDataStorage: false,
versionHashModifier: versionHashModifier(),
renamingIdentifier: renamingIdentifier(),
valueTransformer: nil,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: initial().cs_toFieldStoredNativeType()
)
@@ -334,7 +339,7 @@ extension FieldContainer.Stored where V: FieldOptionalType {
- parameter initial: the initial value for the property when the object is first create
- parameter keyPath: the permanent attribute name for this property.
- parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.)
- parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name.
- parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`.
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.primitiveValue(for:)` instead of `PartialObject<O>.value(for:)`, which would unintentionally execute the same closure again recursively.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject<O>`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject<O>`, make sure to use `PartialObject<O>.setPrimitiveValue(_:for:)` instead of `PartialObject<O>.setValue(_:for:)`, which would unintentionally execute the same closure again recursively.
- parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`.
@@ -343,7 +348,7 @@ extension FieldContainer.Stored where V: FieldOptionalType {
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
renamingIdentifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ partialObject: PartialObject<O>) -> V)? = nil,
customSetter: ((_ partialObject: PartialObject<O>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
@@ -353,7 +358,7 @@ extension FieldContainer.Stored where V: FieldOptionalType {
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: renamingIdentifier,
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths

View File

@@ -0,0 +1,49 @@
//
// FieldAttributeProtocol.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
// MARK: - FieldAttributeProtocol
internal protocol FieldAttributeProtocol: FieldProtocol {
typealias EntityDescriptionValues = (
attributeType: NSAttributeType,
isOptional: Bool,
isTransient: Bool,
allowsExternalBinaryDataStorage: Bool,
versionHashModifier: String?,
renamingIdentifier: String?,
valueTransformer: Internals.AnyFieldCoder?,
affectedByKeyPaths: Set<KeyPathString>,
defaultValue: Any?
)
var entityDescriptionValues: () -> EntityDescriptionValues { get }
var getter: CoreStoreManagedObject.CustomGetter? { get }
var setter: CoreStoreManagedObject.CustomSetter? { get }
}

View File

@@ -0,0 +1,38 @@
//
// FieldCoderType.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
// MARK: - FieldCoderType
public protocol FieldCoderType {
associatedtype FieldStoredValue
static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data?
static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue?
}

View File

@@ -0,0 +1,87 @@
//
// FieldCoders.DefaultNSSecureCoding.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - FieldCoders
extension FieldCoders {
public struct DefaultNSSecureCoding<T: DefaultNSSecureCodable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = T
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
return ValueTransformer(forName: self.transformerName)?.reverseTransformedValue(fieldValue) as? Data
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
return ValueTransformer(forName: self.transformerName)?.transformedValue(data) as! FieldStoredValue?
}
// MARK: Internal
internal static var transformerName: NSValueTransformerName {
if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) {
return .secureUnarchiveFromDataTransformerName
}
else {
return .keyedUnarchiveFromDataTransformerName
}
}
}
}
public protocol DefaultNSSecureCodable: NSObject, NSSecureCoding {}
extension NSArray: DefaultNSSecureCodable {}
extension NSDictionary: DefaultNSSecureCodable {}
extension NSSet: DefaultNSSecureCodable {}
extension NSString: DefaultNSSecureCodable {}
extension NSNumber: DefaultNSSecureCodable {}
extension NSDate: DefaultNSSecureCodable {}
extension NSData: DefaultNSSecureCodable {}
extension NSURL: DefaultNSSecureCodable {}
extension NSUUID: DefaultNSSecureCodable {}
extension NSNull: DefaultNSSecureCodable {}

View File

@@ -0,0 +1,59 @@
//
// FieldCoders.Json.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - Json
public struct Json<V: Codable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
return try? JSONEncoder().encode(fieldValue)
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
return try? JSONDecoder().decode(FieldStoredValue.self, from: data)
}
}
}

View File

@@ -0,0 +1,91 @@
//
// FieldCoders.NSCoding.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - NSCoding
public struct NSCoding<V: NSObject & Foundation.NSCoding>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) {
return try! NSKeyedArchiver.archivedData(
withRootObject: fieldValue,
requiringSecureCoding: self.requiresSecureCoding
)
}
else {
return NSKeyedArchiver.archivedData(withRootObject: fieldValue)
}
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) {
return try! NSKeyedUnarchiver.unarchivedObject(ofClass: FieldStoredValue.self, from: data)
}
else {
return NSKeyedUnarchiver.unarchiveObject(with: data) as! FieldStoredValue?
}
}
// MARK: Private
private static var requiresSecureCoding: Bool {
switch FieldStoredValue.self {
case let valueType as NSSecureCoding.Type:
return valueType.supportsSecureCoding
default:
return false
}
}
}
}

View File

@@ -0,0 +1,59 @@
//
// FieldCoders.Plist.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - Plist
public struct Plist<V: Codable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
return try? PropertyListEncoder().encode(fieldValue)
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
return try? PropertyListDecoder().decode(FieldStoredValue.self, from: data)
}
}
}

30
Sources/FieldCoders.swift Normal file
View File

@@ -0,0 +1,30 @@
//
// FieldCoders.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
/**
Namespace for Built-in Field Coders
*/
public enum FieldCoders {}

View File

@@ -34,24 +34,3 @@ internal protocol FieldProtocol: PropertyProtocol {
static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any?
static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?)
}
// MARK: - FieldAttributeProtocol
internal protocol FieldAttributeProtocol: FieldProtocol {
typealias EntityDescriptionValues = (
attributeType: NSAttributeType,
isOptional: Bool,
isTransient: Bool,
allowsExternalBinaryDataStorage: Bool,
versionHashModifier: String?,
renamingIdentifier: String?,
affectedByKeyPaths: Set<KeyPathString>,
defaultValue: Any?
)
var entityDescriptionValues: () -> EntityDescriptionValues { get }
var getter: CoreStoreManagedObject.CustomGetter? { get }
var setter: CoreStoreManagedObject.CustomSetter? { get }
}

View File

@@ -0,0 +1,222 @@
//
// Internals.AnyFieldCoder.swift
// CoreStore
//
// Copyright © 2020 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
// MARK: - Internals
extension Internals {
// MARK: - AnyFieldCoder
internal struct AnyFieldCoder {
// MARK: Internal
internal let transformerName: NSValueTransformerName
internal let transformer: Foundation.ValueTransformer
internal let encodeToStoredData: (_ fieldValue: Any?) -> Data?
internal let decodeFromStoredData: (_ data: Data?) -> Any?
internal init<Coder: FieldCoderType>(_ fieldCoder: Coder.Type) {
let transformer = CustomValueTransformer(fieldCoder: fieldCoder)
self.transformerName = transformer.id
self.transformer = transformer
self.encodeToStoredData = {
switch $0 {
case let value as Coder.FieldStoredValue:
return fieldCoder.encodeToStoredData(value)
case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
return fieldCoder.encodeToStoredData(valueBox.value as! Coder.FieldStoredValue?)
default:
return fieldCoder.encodeToStoredData(nil)
}
}
self.decodeFromStoredData = { fieldCoder.decodeFromStoredData($0) }
}
internal init<V>(tag: UUID, encode: @escaping (V) -> Data?, decode: @escaping (Data?) -> V) {
let transformer = CustomValueTransformer(tag: tag)
self.transformerName = transformer.id
self.transformer = transformer
self.encodeToStoredData = { encode($0 as! V) }
self.decodeFromStoredData = { decode($0) }
}
internal func register() {
let transformerName = self.transformerName
if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) {
if transformerName == .secureUnarchiveFromDataTransformerName {
return
}
}
switch transformerName {
case .keyedUnarchiveFromDataTransformerName,
.unarchiveFromDataTransformerName,
.isNotNilTransformerName,
.isNilTransformerName,
.negateBooleanTransformerName:
return
case let transformerName:
Self.cachedCoders[transformerName] = self
Foundation.ValueTransformer.setValueTransformer(
self.transformer,
forName: transformerName
)
}
}
// MARK: FilePrivate
fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:]
// MARK: - TransformableDefaultValueCodingBox
@objc(_CoreStore_Internals_TransformableDefaultValueCodingBox)
internal final class TransformableDefaultValueCodingBox: NSObject, NSSecureCoding {
// MARK: Internal
@objc
internal dynamic let transformerName: String
@objc
internal dynamic let data: Data
@nonobjc
internal let value: Any?
internal init?(defaultValue: Any?, fieldCoder: Internals.AnyFieldCoder?) {
guard
let fieldCoder = fieldCoder,
let defaultValue = defaultValue,
!(defaultValue is NSNull),
let data = fieldCoder.encodeToStoredData(defaultValue)
else {
return nil
}
self.transformerName = fieldCoder.transformerName.rawValue
self.value = defaultValue
self.data = data
}
// MARK: NSSecureCoding
@objc
dynamic class var supportsSecureCoding: Bool {
return true
}
// MARK: NSCoding
@objc
dynamic required init?(coder aDecoder: NSCoder) {
guard
case let transformerName as String = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName)),
let transformer = ValueTransformer(forName: .init(transformerName)),
case let data as Data = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.data)),
let value = transformer.reverseTransformedValue(data)
else {
return nil
}
self.transformerName = transformerName
self.data = data
self.value = value
}
@objc
dynamic func encode(with coder: NSCoder) {
coder.encode(self.data, forKey: #keyPath(TransformableDefaultValueCodingBox.data))
coder.encode(self.transformerName, forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName))
}
}
}
// MARK: - CustomValueTransformer
fileprivate final class CustomValueTransformer: ValueTransformer {
// MARK: FilePrivate
fileprivate let id: NSValueTransformerName
fileprivate init<Coder: FieldCoderType>(fieldCoder: Coder.Type) {
self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(String(reflecting: fieldCoder))>.transformerName")
}
fileprivate init(tag: UUID) {
self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(tag.uuidString)>.transformerName")
}
// MARK: ValueTransformer
override class func transformedValueClass() -> AnyClass {
return NSData.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
return AnyFieldCoder.cachedCoders[self.id]?.encodeToStoredData(value) as Data?
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
return AnyFieldCoder.cachedCoders[self.id]?.decodeFromStoredData(value as! Data?)
}
}
}

View File

@@ -33,29 +33,49 @@ extension NSEntityDescription {
@nonobjc
internal func cs_resolveAttributeNames() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] {
return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.name] = (attribute.description, attribute.description.versionHash)
})
return self.attributesByName.reduce(
into: [:],
{ (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.name] = (attribute.description, attribute.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveAttributeRenamingIdentities() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] {
return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash)
})
return self.attributesByName.reduce(
into: [:],
{ (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRelationshipNames() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] {
return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.name] = (relationship.description, relationship.description.versionHash)
})
return self.relationshipsByName.reduce(
into: [:],
{ (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.name] = (relationship.description, relationship.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRelationshipRenamingIdentities() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] {
return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash)
})
return self.relationshipsByName.reduce(
into: [:],
{ (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash)
}
)
}
}

View File

@@ -33,15 +33,25 @@ extension NSManagedObjectModel {
@nonobjc
internal func cs_resolveNames() -> [String: (entity: NSEntityDescription, versionHash: Data)] {
return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.name] = (entity.description, entity.description.versionHash)
})
return self.entitiesByName.reduce(
into: [:],
{ (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.name] = (entity.description, entity.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRenamingIdentities() -> [String: (entity: NSEntityDescription, versionHash: Data)] {
return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash)
})
return self.entitiesByName.reduce(
into: [:],
{ (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash)
}
)
}
}

View File

@@ -65,8 +65,10 @@ public struct PartialObject<O: CoreStoreObject> {
case let value as V:
return value
default:
return nil as Any? as! V // filter NSNull
case nil,
is NSNull,
_? /* any other unrelated type */ :
return nil as Any? as! V // There should be no uninitialized state at this point
}
}
@@ -87,15 +89,41 @@ public struct PartialObject<O: CoreStoreObject> {
This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receivers private storage.
*/
public func primitiveValue<V>(for property: (O) -> FieldContainer<O>.Computed<V>) -> V {
public func primitiveValue<V>(for property: (O) -> FieldContainer<O>.Computed<V>) -> V? {
switch self.rawObject.primitiveValue(forKey: property(O.meta).keyPath) {
case let value as V:
return value
default:
return nil as Any? as! V // filter NSNull
case nil,
is NSNull,
_? /* any other unrelated type */ :
return nil
}
}
/**
Returns the value for the specified property from the managed objects private internal storage.
This method does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily by subclasses that implement custom accessor methods that need direct access to the receivers private storage.
*/
public func primitiveValue<V>(for property: (O) -> FieldContainer<O>.Coded<V>) -> V? {
let keyPath = property(O.meta).keyPath
switch self.rawObject.primitiveValue(forKey: keyPath) {
case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
rawObject.setPrimitiveValue(valueBox.value, forKey: keyPath)
return valueBox.value as? V
case let value as V:
return value
case nil,
is NSNull,
_? /* any other unrelated type */ :
return nil
}
}