diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index eba30a4..106f7f5 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -114,7 +114,8 @@ class Person: CoreStoreObject { var customField: CustomType @Field.Coded( - "job", coder: ( + "job", + coder: ( encode: { $0.toData() }, decode: { $0.flatMap(Job.init(data:)) ?? .unemployed } ) diff --git a/Sources/DynamicSchema+Convenience.swift b/Sources/DynamicSchema+Convenience.swift index c70ccc4..4f84648 100644 --- a/Sources/DynamicSchema+Convenience.swift +++ b/Sources/DynamicSchema+Convenience.swift @@ -68,89 +68,124 @@ extension DynamicSchema { for (attributeName, attribute) in attributesByName { let containerType: String - if attribute.attributeType == .transformableAttributeType { - - if attribute.isOptional { - - containerType = "Transformable.Optional" - } - else { - - containerType = "Transformable.Required" - } + if attribute.isTransient || attribute.attributeType == .undefinedAttributeType { + + containerType = "Field.Computed" + } + else if attribute.attributeType == .transformableAttributeType { + + containerType = "Field.Coded" } else { - - if attribute.isOptional { - - containerType = "Value.Optional" - } - else { - - containerType = "Value.Required" - } + + containerType = "Field.Stored" } - let valueType: Any.Type + var valueTypeString: String var defaultString = "" + var coderString = "" switch attribute.attributeType { case .integer16AttributeType: - valueType = Int16.self + valueTypeString = String(describing: Int16.self) if let defaultValue = (attribute.defaultValue as! Int16.QueryableNativeType?).flatMap(Int16.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .integer32AttributeType: - valueType = Int32.self + valueTypeString = String(describing: Int32.self) if let defaultValue = (attribute.defaultValue as! Int32.QueryableNativeType?).flatMap(Int32.cs_fromQueryableNativeType) { - - defaultString = ", initial: \(defaultValue)" + + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .integer64AttributeType: - valueType = Int64.self + valueTypeString = String(describing: Int64.self) if let defaultValue = (attribute.defaultValue as! Int64.QueryableNativeType?).flatMap(Int64.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .decimalAttributeType: - valueType = NSDecimalNumber.self + valueTypeString = String(describing: NSDecimalNumber.self) if let defaultValue = (attribute.defaultValue as! NSDecimalNumber?) { - defaultString = ", initial: NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" + defaultString = " = NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .doubleAttributeType: - valueType = Double.self + valueTypeString = String(describing: Double.self) if let defaultValue = (attribute.defaultValue as! Double.QueryableNativeType?).flatMap(Double.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .floatAttributeType: - valueType = Float.self + valueTypeString = String(describing: Float.self) if let defaultValue = (attribute.defaultValue as! Float.QueryableNativeType?).flatMap(Float.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue)" + defaultString = " = \(defaultValue)" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .stringAttributeType: - valueType = String.self + valueTypeString = String(describing: String.self) if let defaultValue = (attribute.defaultValue as! String.QueryableNativeType?).flatMap(String.cs_fromQueryableNativeType) { - - // TODO: escape strings - defaultString = ", initial: \"\(defaultValue)\"" + + defaultString = " = \"\(defaultValue.replacingOccurrences(of: "\\", with: "\\\\"))\"" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .booleanAttributeType: - valueType = Bool.self + valueTypeString = String(describing: Bool.self) if let defaultValue = (attribute.defaultValue as! Bool.QueryableNativeType?).flatMap(Bool.cs_fromQueryableNativeType) { - defaultString = ", initial: \(defaultValue ? "true" : "false")" + defaultString = " = \(defaultValue ? "true" : "false")" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .dateAttributeType: - valueType = Date.self + valueTypeString = String(describing: Date.self) if let defaultValue = (attribute.defaultValue as! Date.QueryableNativeType?).flatMap(Date.cs_fromQueryableNativeType) { - defaultString = ", initial: Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" + defaultString = " = Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .binaryDataAttributeType: - valueType = Data.self + valueTypeString = String(describing: Data.self) if let defaultValue = (attribute.defaultValue as! Data.QueryableNativeType?).flatMap(Data.cs_fromQueryableNativeType) { let bytes = defaultValue.withUnsafeBytes { (pointer) in @@ -158,49 +193,106 @@ extension DynamicSchema { .bindMemory(to: UInt64.self) .map({ "\("0x\(String($0, radix: 16, uppercase: false))")" }) } - defaultString = ", initial: Data(bytes: [\(bytes.joined(separator: ", "))])" + defaultString = " = Data(bytes: [\(bytes.joined(separator: ", "))])" + } + else if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" } case .transformableAttributeType: + if let valueTransformerName = attribute.valueTransformerName { + + coderString = ", coder: /* Required compatible FieldCoderType implementation for ValueTransformer named \"\(valueTransformerName)\" */" + } + else { + + coderString = ", coder: FieldCoders.NSCoding.self" + } if let attributeValueClassName = attribute.attributeValueClassName { - - valueType = NSClassFromString(attributeValueClassName)! + + valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) + } + else { + + valueTypeString = "/* */" + } + if let defaultValue = attribute.defaultValue { + + switch defaultValue { + + case let defaultValueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + if let defaultValue = defaultValueBox.value { + + defaultString = " = /* \"\(defaultValue)\" */" + } + else if attribute.isOptional { + + defaultString = " = nil" + } + else { + + defaultString = " = /* */" + } + + case let defaultValue: + defaultString = " = /* \"\(defaultValue)\" */" + } + } + else if attribute.isOptional { + + defaultString = " = nil" } else { - valueType = (NSCoding & NSCopying).self + defaultString = " = /* */" } - if let defaultValue = attribute.defaultValue { - - defaultString = ", initial: /* \"\(defaultValue)\" */" + if attribute.isOptional { + + valueTypeString += "?" } - else if !attribute.isOptional { - - defaultString = ", initial: /* required */" + + case .undefinedAttributeType where attribute.isTransient: + coderString = ", customGetter: \\* *\\" + if let attributeValueClassName = attribute.attributeValueClassName { + + valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) } - case .undefinedAttributeType: - #warning("TODO: Field.Computed") - continue + else { + + valueTypeString = " = /* */" + } + if attribute.isOptional { + + valueTypeString += "?" + defaultString = " = nil" + } + default: fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)") } - let transientString = attribute.isTransient ? ", isTransient: true" : "" - // TODO: escape strings let versionHashModifierString = attribute.versionHashModifier - .flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? "" - // TODO: escape strings + .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" + let renamingIdentifierString = attribute.renamingIdentifier - .flatMap({ ($0 == attributeName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? "" - output.append(" let \(attributeName) = \(containerType)<\(String(describing: valueType))>(\"\(attributeName)\"\(defaultString)\(transientString)\(versionHashModifierString)\(renamingIdentifierString))\n") + .map({ ($0 == attributeName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" + if attributeName.hasPrefix("_") { + + output.append(" #warning(\"Field variable names cannot start with underscores)") + } + output.append(" @\(containerType)(\"\(attributeName)\"\(versionHashModifierString)\(renamingIdentifierString)\(coderString))\n") + output.append(" var \(attributeName): \(valueTypeString)\(defaultString)\n\n") } } let relationshipsByName = entity.relationshipsByName if !relationshipsByName.isEmpty { - + output.append(" \n") for (relationshipName, relationship) in relationshipsByName { let containerType: String + let destinationEntityName = relationship.destinationEntity!.name! var minCountString = "" var maxCountString = "" if relationship.isToMany { @@ -209,11 +301,11 @@ extension DynamicSchema { let maxCount = relationship.maxCount if relationship.isOrdered { - containerType = "Relationship.ToManyOrdered" + containerType = "[\(destinationEntityName)]" } else { - containerType = "Relationship.ToManyUnordered" + containerType = "Set<\(destinationEntityName)>" } if minCount > 0 { @@ -225,16 +317,16 @@ extension DynamicSchema { } } else { - - containerType = "Relationship.ToOne" + + containerType = "\(destinationEntityName)?" } var inverseString = "" let relationshipQualifier = "\(entityName).\(relationshipName)" if !addedInverse.contains(relationshipQualifier), let inverseRelationship = relationship.inverseRelationship { - inverseString = ", inverse: { $0.\(inverseRelationship.name) }" - addedInverse.insert("\(relationship.destinationEntity!.name!).\(inverseRelationship.name)") + inverseString = ", inverse: \\.$\(inverseRelationship.name)" + addedInverse.insert("\(destinationEntityName).\(inverseRelationship.name)") } var deleteRuleString = "" if relationship.deleteRule != .nullifyDeleteRule { @@ -255,10 +347,15 @@ extension DynamicSchema { } } let versionHashModifierString = relationship.versionHashModifier - .flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? "" + .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" let renamingIdentifierString = relationship.renamingIdentifier - .flatMap({ ($0 == relationshipName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? "" - output.append(" let \(relationshipName) = \(containerType)<\(relationship.destinationEntity!.name!)>(\"\(relationshipName)\"\(inverseString)\(deleteRuleString)\(minCountString)\(maxCountString)\(versionHashModifierString)\(renamingIdentifierString))\n") + .map({ ($0 == relationshipName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" + if relationshipName.hasPrefix("_") { + + output.append(" #error(\"Field variable names cannot start with underscores)\n") + } + output.append(" @Field.Relationship(\"\(relationshipName)\"\(minCountString)\(maxCountString)\(inverseString)\(deleteRuleString)\(versionHashModifierString)\(renamingIdentifierString))\n") + output.append(" var \(relationshipName): \(containerType)\n\n") } } } diff --git a/Sources/Field.Relationship.swift b/Sources/Field.Relationship.swift index 005dff9..630941b 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -37,7 +37,40 @@ extension FieldContainer { // @dynamicMemberLookup public struct Relationship: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol { - public typealias DeleteRule = RelationshipContainer.DeleteRule + /** + Overload for compiler error message only + */ + @available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.") + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil + ) { + + fatalError() + } + + /** + Overload for compiler error message only + */ + @available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.") + public init( + wrappedValue initial: @autoclosure @escaping () -> V, + _ keyPath: KeyPathString, + minCount: Int = 0, + maxCount: Int = 0, + inverse: KeyPath.Relationship>, + deleteRule: DeleteRule = .nullify, + versionHashModifier: @autoclosure @escaping () -> String? = nil, + previousVersionKeyPath: @autoclosure @escaping () -> String? = nil + ) { + + fatalError() + } // MARK: @propertyWrapper @@ -188,6 +221,45 @@ extension FieldContainer { ) } } + + + // MARK: - DeleteRule + + /** + These constants define what happens to relationships when an object is deleted. + */ + public enum DeleteRule { + + // MARK: Public + + /** + If the object is deleted, back pointers from the objects to which it is related are nullified. + */ + case nullify + + /** + If the object is deleted, the destination object or objects of this relationship are also deleted. + */ + case cascade + + /** + If the destination of this relationship is not nil, the delete creates a validation error. + */ + case deny + + + // MARK: Internal + + internal var nativeValue: NSDeleteRule { + + switch self { + + case .nullify: return .nullifyDeleteRule + case .cascade: return .cascadeDeleteRule + case .deny: return .denyDeleteRule + } + } + } } } @@ -271,7 +343,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp _ keyPath: KeyPathString, minCount: Int = 0, maxCount: Int = 0, - inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + inverse: KeyPath.Relationship>, deleteRule: DeleteRule = .nullify, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, @@ -283,7 +355,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedTyp isToMany: true, isOrdered: true, deleteRule: deleteRule, - inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath }, versionHashModifier: versionHashModifier, renamingIdentifier: previousVersionKeyPath, affectedByKeyPaths: affectedByKeyPaths, @@ -323,7 +395,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedT _ keyPath: KeyPathString, minCount: Int = 0, maxCount: Int = 0, - inverse: @escaping (V.DestinationObjectType) -> FieldContainer.Relationship, + inverse: KeyPath.Relationship>, deleteRule: DeleteRule = .nullify, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, @@ -335,7 +407,7 @@ extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedT isToMany: true, isOrdered: false, deleteRule: deleteRule, - inverseKeyPath: { inverse(V.DestinationObjectType.meta).keyPath }, + inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath }, versionHashModifier: versionHashModifier, renamingIdentifier: previousVersionKeyPath, affectedByKeyPaths: affectedByKeyPaths, diff --git a/Sources/FieldCoders.Json.swift b/Sources/FieldCoders.Json.swift index 02de3a3..f16a224 100644 --- a/Sources/FieldCoders.Json.swift +++ b/Sources/FieldCoders.Json.swift @@ -44,7 +44,7 @@ extension FieldCoders { return nil } - return try? JSONEncoder().encode(fieldValue) + return try! JSONEncoder().encode([fieldValue]) } public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { @@ -53,7 +53,7 @@ extension FieldCoders { return nil } - return try? JSONDecoder().decode(FieldStoredValue.self, from: data) + return try! JSONDecoder().decode([FieldStoredValue].self, from: data).first } } } diff --git a/Sources/FieldCoders.NSCoding.swift b/Sources/FieldCoders.NSCoding.swift index 672e8d3..150e081 100644 --- a/Sources/FieldCoders.NSCoding.swift +++ b/Sources/FieldCoders.NSCoding.swift @@ -32,7 +32,7 @@ extension FieldCoders { // MARK: - NSCoding - public struct NSCoding: FieldCoderType { + public struct NSCoding: FieldCoderType { // MARK: FieldCoderType diff --git a/Sources/FieldCoders.Plist.swift b/Sources/FieldCoders.Plist.swift index ffc1908..1055ec5 100644 --- a/Sources/FieldCoders.Plist.swift +++ b/Sources/FieldCoders.Plist.swift @@ -44,7 +44,7 @@ extension FieldCoders { return nil } - return try? PropertyListEncoder().encode(fieldValue) + return try! PropertyListEncoder().encode([fieldValue]) } public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? { @@ -53,7 +53,7 @@ extension FieldCoders { return nil } - return try? PropertyListDecoder().decode(FieldStoredValue.self, from: data) + return try! PropertyListDecoder().decode([FieldStoredValue].self, from: data).first } } } diff --git a/Sources/Relationship.swift b/Sources/Relationship.swift index 212ea09..b61052e 100644 --- a/Sources/Relationship.swift +++ b/Sources/Relationship.swift @@ -63,7 +63,10 @@ extension DynamicObject where Self: CoreStoreObject { public enum RelationshipContainer { // MARK: - DeleteRule - + + /** + These constants define what happens to relationships when an object is deleted. + */ public enum DeleteRule { // MARK: Public