Skip to content

Add support for transformable property types #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Sources/ManagedModels/SchemaCompatibility/CodableBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ final class CodableBox<T: Codable>: 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<T>(baseTyped)
}
guard let typed = value as? CodableBox<T> else {
assertionFailure("Value to be transformed is not the box? \(value)")
return nil
Expand All @@ -77,7 +80,7 @@ final class CodableBox<T: Codable>: 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<T>(data: data)
return CodableBox<T>(data: data)?.value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
}

Expand All @@ -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, *) {
Expand Down
40 changes: 34 additions & 6 deletions Tests/ManagedModelTests/SchemaGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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
Expand All @@ -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)
}
}

Expand Down
8 changes: 7 additions & 1 deletion Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,26 @@ extension Fixtures {
var addresses : Set<Address> // [ 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
}
}
Expand Down
59 changes: 59 additions & 0 deletions Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift
Original file line number Diff line number Diff line change
@@ -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])
}
}
}
}
72 changes: 72 additions & 0 deletions Tests/ManagedModelTests/TransformablePropertiesTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}