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 } } } diff --git a/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift b/Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift index 66672c9..7241cc2 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 { + 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..9a920af --- /dev/null +++ b/Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift @@ -0,0 +1,59 @@ +// +// 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 + @Attribute(.transformable(by: AccessSIPTransformer.self)) + var optionalSIP : 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..1a0774b --- /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 let 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) + } +}