From e2f7b09c35d7c7f1e750dac5f54e1b93712271bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Sun, 11 Feb 2024 17:28:39 +0100 Subject: [PATCH 1/5] Proposed a fix for Transformable property as per issue #4 --- .../SchemaCompatibility/CodableBox.swift | 6 +- .../NSAttributeDescription+Data.swift | 24 +++++-- .../SchemaGenerationTests.swift | 40 +++++++++-- .../Schemas/PersonAddressSchema.swift | 8 ++- .../Schemas/TransformablePropertySchema.swift | 57 +++++++++++++++ .../TransformablePropertiesTests.swift | 72 +++++++++++++++++++ 6 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift create mode 100644 Tests/ManagedModelTests/TransformablePropertiesTests.swift diff --git a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift index 7a42d4b..9589f1e 100644 --- a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift +++ b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift @@ -57,13 +57,17 @@ final class CodableBox: NSObject, NSCopying { } } - final class Transformer: ValueTransformer { + final class Transformer: NSSecureUnarchiveFromDataTransformer { override class func transformedValueClass() -> AnyClass { CodableBox.self } override class func allowsReverseTransformation() -> Bool { true } + override class var allowedTopLevelClasses: [AnyClass] { + [ CodableBox.self, NSData.self ] + } + override func transformedValue(_ value: Any?) -> Any? { // value is the box guard let value else { return nil } diff --git a/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift b/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift index 66672c9..99e5744 100644 --- a/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift +++ b/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift @@ -92,6 +92,15 @@ extension CoreData.NSAttributeDescription: SchemaProperty { return } + if let valueTransformerName = valueTransformerName, newValue is any NSObjectProtocol.Type { + self.attributeType = .transformableAttributeType + self.isOptional = newValue is any AnyOptional.Type + + self.attributeValueClassName = NSStringFromClass(NSObject.self) + assert(ValueTransformer.valueTransformerNames().contains(.init(valueTransformerName))) + return + } + // TBD: // undefinedAttributeType = 0 // transformableAttributeType = 1800 @@ -168,9 +177,8 @@ public extension NSAttributeDescription { assert(valueTransformerName == nil) valueTransformerName = nil - if valueType != Any.self { self.valueType = valueType } - setOptions(options) + if valueType != Any.self { self.valueType = valueType } } } @@ -196,11 +204,17 @@ private extension NSAttributeDescription { case .ephemeral: isTransient = true case .transformableByName(let name): - assert(valueTransformerName == nil) - valueTransformerName = name + fatalError("Not supported") case .transformableByType(let type): assert(valueTransformerName == nil) - valueTransformerName = NSStringFromClass(type) + let name = NSStringFromClass(type) + if !ValueTransformer.valueTransformerNames().contains(.init(name)) { + // no access to valueTransformerForName? + let transformer = type.init() + ValueTransformer + .setValueTransformer(transformer, forName: .init(name)) + } + valueTransformerName = name case .allowsCloudEncryption: // FIXME: restrict availability if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { diff --git a/Tests/ManagedModelTests/SchemaGenerationTests.swift b/Tests/ManagedModelTests/SchemaGenerationTests.swift index 4ff7919..c8f8634 100644 --- a/Tests/ManagedModelTests/SchemaGenerationTests.swift +++ b/Tests/ManagedModelTests/SchemaGenerationTests.swift @@ -102,7 +102,7 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertEqual (toAddresses.destination, "Address") XCTAssertNotNil(toAddresses.destinationEntity) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) XCTAssertEqual(address.relationships.count, 1) let toPerson = try XCTUnwrap(address.relationshipsByName["person"]) XCTAssertTrue (toPerson.isRelationship) @@ -172,7 +172,7 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertEqual(schema.entitiesByName.count, 2) let address = try XCTUnwrap(schema.entitiesByName["Address"]) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) let appartment = try XCTUnwrap(address.attributesByName["appartment"]) XCTAssertFalse(appartment.isTransient) @@ -189,6 +189,34 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertEqual(street.attributeType, .stringAttributeType) } + func testRawRepresentableEnum() throws { + let cache = SchemaBuilder() + let schema = NSManagedObjectModel( + [ Fixtures.PersonAddressSchema.Person.self ], + schemaCache: cache + ) + + XCTAssertEqual(schema.entities.count, 2) + XCTAssertEqual(schema.entitiesByName.count, 2) + + let address = try XCTUnwrap(schema.entitiesByName["Address"]) + XCTAssertEqual(address.attributes.count, 3) + + let street = try XCTUnwrap(address.attributesByName["street"]) + XCTAssertFalse(street.isTransient) + XCTAssertFalse(street.isRelationship) + XCTAssertTrue (street.isAttribute) + XCTAssertFalse(street.isOptional) + XCTAssertEqual(street.attributeType, .stringAttributeType) + + let type = try XCTUnwrap(address.attributesByName["type"]) + XCTAssertFalse(type.isTransient) + XCTAssertFalse(type.isRelationship) + XCTAssertTrue (type.isAttribute) + XCTAssertFalse(type.isOptional) + XCTAssertEqual(type.attributeType, .integer64AttributeType) + } + func testMOM() throws { let mom = Fixtures.PersonAddressMOM XCTAssertEqual(mom.entities.count, 2) @@ -208,7 +236,7 @@ final class SchemaGenerationTests: XCTestCase { let address = try XCTUnwrap( entities.first(where: { $0.name == "Address" }) ) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) } // second run @@ -221,7 +249,7 @@ final class SchemaGenerationTests: XCTestCase { let address = try XCTUnwrap( entities.first(where: { $0.name == "Address" }) ) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) } } @@ -235,7 +263,7 @@ final class SchemaGenerationTests: XCTestCase { let address = try XCTUnwrap( model1.entities.first(where: { $0.name == "Address" }) ) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) } // second run @@ -248,7 +276,7 @@ final class SchemaGenerationTests: XCTestCase { let address = try XCTUnwrap( model2.entities.first(where: { $0.name == "Address" }) ) - XCTAssertEqual(address.attributes.count, 2) + XCTAssertEqual(address.attributes.count, 3) } } diff --git a/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift b/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift index 9fc368f..a4e8920 100644 --- a/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift +++ b/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift @@ -26,20 +26,26 @@ extension Fixtures { var addresses : Set
// [ Address ] } + enum AddressType: Int { + case home, work + } + @Model final class Address /*test*/ : NSManagedObject { var street : String var appartment : String? + var type : AddressType var person : Person // Either: super.init(entity: Self.entity(), insertInto: nil) // Or: mark this as `convenience` - convenience init(street: String, appartment: String? = nil, person: Person) { + convenience init(street: String, appartment: String? = nil, type: AddressType, person: Person) { //super.init(entity: Self.entity(), insertInto: nil) self.init() self.street = street self.appartment = appartment + self.type = type self.person = person } } diff --git a/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift b/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift new file mode 100644 index 0000000..9f37ba3 --- /dev/null +++ b/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift @@ -0,0 +1,57 @@ +// +// TransformablePropertySchema.swift +// Created by Adam Kopeć on 11/02/2024. +// + +import ManagedModels + +extension Fixtures { + // https://github.com/Data-swift/ManagedModels/issues/4 + + enum TransformablePropertiesSchema: VersionedSchema { + static var models : [ any PersistentModel.Type ] = [ + StoredAccess.self + ] + + public static let versionIdentifier = Schema.Version(0, 1, 0) + + @Model + final class StoredAccess: NSManagedObject { + var token : String + var expires : Date + @Attribute(.transformable(by: AccessSIPTransformer.self)) + var sip : AccessSIP + } + + class AccessSIP: NSObject { + var username : String + var password : String + + init(username: String, password: String) { + self.username = username + self.password = password + } + } + + class AccessSIPTransformer: ValueTransformer { + override class func transformedValueClass() -> AnyClass { + return AccessSIP.self + } + + override class func allowsReverseTransformation() -> Bool { + return true + } + + override func transformedValue(_ value: Any?) -> Any? { + guard let data = value as? Data else { return nil } + guard let array = try? JSONDecoder().decode([String].self, from: data) else { return nil } + return AccessSIP(username: array[0], password: array[1]) + } + + override func reverseTransformedValue(_ value: Any?) -> Any? { + guard let sip = value as? AccessSIP else { return nil } + return try? JSONEncoder().encode([sip.username, sip.password]) + } + } + } +} diff --git a/Tests/ManagedModelTests/TransformablePropertiesTests.swift b/Tests/ManagedModelTests/TransformablePropertiesTests.swift new file mode 100644 index 0000000..21be9be --- /dev/null +++ b/Tests/ManagedModelTests/TransformablePropertiesTests.swift @@ -0,0 +1,72 @@ +// +// TransformablePropertiesTests.swift +// Created by Adam Kopeć on 11/02/2024. +// + +import XCTest +import Foundation +import CoreData +@testable import ManagedModels + +final class TransformablePropertiesTests: XCTestCase { + + private lazy var container = try? ModelContainer( + for: Fixtures.TransformablePropertiesSchema.managedObjectModel, + configurations: ModelConfiguration(isStoredInMemoryOnly: true) + ) + + func testEntityName() throws { + let entityType = Fixtures.TransformablePropertiesSchema.StoredAccess.self + XCTAssertEqual(entityType.entity().name, "StoredAccess") + } + + func testPropertySetup() throws { + let valueType = Fixtures.TransformablePropertiesSchema.AccessSIP.self + let attribute = CoreData.NSAttributeDescription( + name: "sip", + options: [.transformable(by: Fixtures.TransformablePropertiesSchema.AccessSIPTransformer.self)], + valueType: valueType, + defaultValue: nil + ) + XCTAssertEqual(attribute.name, "sip") + XCTAssertEqual(attribute.attributeType, .transformableAttributeType) + + let transformerName = try XCTUnwrap( + ValueTransformer.valueTransformerNames().first(where: { + $0.rawValue.range(of: "AccessSIPTransformer") + != nil + }) + ) + let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) + _ = transformer // to clear unused-wraning + + XCTAssertTrue(attribute.valueType == + NSObject.self) + XCTAssertNotNil(attribute.valueTransformerName) + XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) + } + + func testTransformablePropertyEntity() throws { + let entity = try XCTUnwrap( + container?.managedObjectModel.entitiesByName["StoredAccess"] + ) + + // Creating the entity should have registered the transformer for the + // CodableBox. + let transformerName = try XCTUnwrap( + ValueTransformer.valueTransformerNames().first(where: { + $0.rawValue.range(of: "AccessSIPTransformer") + != nil + }) + ) + let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) + _ = transformer // to clear unused-wraning + + let attribute = try XCTUnwrap(entity.attributesByName["sip"]) + XCTAssertEqual(attribute.name, "sip") + XCTAssertTrue(attribute.valueType == + NSObject.self) + XCTAssertNotNil(attribute.valueTransformerName) + XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) + } +} From f7f8f5fe6761f4e8cfe241e3994401fce7eaf1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Sun, 11 Feb 2024 17:33:56 +0100 Subject: [PATCH 2/5] Making sure the fix also works with Optionals --- .../SchemaCompatibility/NSAttributeDescription+Data.swift | 2 +- .../ManagedModelTests/Schemas/TransformablePropertySchema.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift b/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift index 99e5744..7241cc2 100644 --- a/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift +++ b/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift @@ -92,7 +92,7 @@ extension CoreData.NSAttributeDescription: SchemaProperty { return } - if let valueTransformerName = valueTransformerName, newValue is any NSObjectProtocol.Type { + if let valueTransformerName = valueTransformerName { self.attributeType = .transformableAttributeType self.isOptional = newValue is any AnyOptional.Type diff --git a/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift b/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift index 9f37ba3..9a920af 100644 --- a/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift +++ b/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift @@ -21,6 +21,8 @@ extension Fixtures { var expires : Date @Attribute(.transformable(by: AccessSIPTransformer.self)) var sip : AccessSIP + @Attribute(.transformable(by: AccessSIPTransformer.self)) + var optionalSIP : AccessSIP? } class AccessSIP: NSObject { From c81b2707b2dc28f3f3459fd69c2dbc281b08b1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Mon, 12 Feb 2024 13:59:34 +0100 Subject: [PATCH 3/5] Fixed broken MOM initialisation in test case --- Tests/ManagedModelTests/TransformablePropertiesTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ManagedModelTests/TransformablePropertiesTests.swift b/Tests/ManagedModelTests/TransformablePropertiesTests.swift index 21be9be..1a0774b 100644 --- a/Tests/ManagedModelTests/TransformablePropertiesTests.swift +++ b/Tests/ManagedModelTests/TransformablePropertiesTests.swift @@ -10,7 +10,7 @@ import CoreData final class TransformablePropertiesTests: XCTestCase { - private lazy var container = try? ModelContainer( + private let container = try? ModelContainer( for: Fixtures.TransformablePropertiesSchema.managedObjectModel, configurations: ModelConfiguration(isStoredInMemoryOnly: true) ) From 4d04acc8cd60a19fa397fa9cd429b5ded4b3d0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Tue, 13 Feb 2024 20:42:56 +0100 Subject: [PATCH 4/5] Aligned with upstream branch --- Sources/ManagedModels/SchemaCompatibility/CodableBox.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift index 9589f1e..7a42d4b 100644 --- a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift +++ b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift @@ -57,17 +57,13 @@ final class CodableBox: NSObject, NSCopying { } } - final class Transformer: NSSecureUnarchiveFromDataTransformer { + final class Transformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { CodableBox.self } override class func allowsReverseTransformation() -> Bool { true } - override class var allowedTopLevelClasses: [AnyClass] { - [ CodableBox.self, NSData.self ] - } - override func transformedValue(_ value: Any?) -> Any? { // value is the box guard let value else { return nil } From 7b8fe9acd6334bd9bfe2982e17785672431cedca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Tue, 13 Feb 2024 20:50:06 +0100 Subject: [PATCH 5/5] Trying to fix issue with Codable transformation to and from base type T --- Sources/ManagedModels/SchemaCompatibility/CodableBox.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift index 7a42d4b..fb45a73 100644 --- a/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift +++ b/Sources/ManagedModels/SchemaCompatibility/CodableBox.swift @@ -66,7 +66,10 @@ final class CodableBox: NSObject, NSCopying { override func transformedValue(_ value: Any?) -> Any? { // value is the box - guard let value else { return nil } + guard var value else { return nil } + if let baseTyped = value as? T { + value = CodableBox(baseTyped) + } guard let typed = value as? CodableBox else { assertionFailure("Value to be transformed is not the box? \(value)") return nil @@ -77,7 +80,7 @@ final class CodableBox: NSObject, NSCopying { override func reverseTransformedValue(_ value: Any?) -> Any? { guard let value else { return nil } guard let data = value as? Data else { return nil } - return CodableBox(data: data) + return CodableBox(data: data)?.value } } }