Skip to content

Commit c1a963c

Browse files
authored
Merge pull request #28 from admkopec/develop
Add support for transformable property types
2 parents 069ccc0 + 7b8fe9a commit c1a963c

File tree

6 files changed

+196
-14
lines changed

6 files changed

+196
-14
lines changed

Sources/ManagedModels/SchemaCompatibility/CodableBox.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ final class CodableBox<T: Codable>: NSObject, NSCopying {
6666

6767
override func transformedValue(_ value: Any?) -> Any? {
6868
// value is the box
69-
guard let value else { return nil }
69+
guard var value else { return nil }
70+
if let baseTyped = value as? T {
71+
value = CodableBox<T>(baseTyped)
72+
}
7073
guard let typed = value as? CodableBox<T> else {
7174
assertionFailure("Value to be transformed is not the box? \(value)")
7275
return nil
@@ -77,7 +80,7 @@ final class CodableBox<T: Codable>: NSObject, NSCopying {
7780
override func reverseTransformedValue(_ value: Any?) -> Any? {
7881
guard let value else { return nil }
7982
guard let data = value as? Data else { return nil }
80-
return CodableBox<T>(data: data)
83+
return CodableBox<T>(data: data)?.value
8184
}
8285
}
8386
}

Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ extension CoreData.NSAttributeDescription: SchemaProperty {
9292
return
9393
}
9494

95+
if let valueTransformerName = valueTransformerName {
96+
self.attributeType = .transformableAttributeType
97+
self.isOptional = newValue is any AnyOptional.Type
98+
99+
self.attributeValueClassName = NSStringFromClass(NSObject.self)
100+
assert(ValueTransformer.valueTransformerNames().contains(.init(valueTransformerName)))
101+
return
102+
}
103+
95104
// TBD:
96105
// undefinedAttributeType = 0
97106
// transformableAttributeType = 1800
@@ -168,9 +177,8 @@ public extension NSAttributeDescription {
168177

169178
assert(valueTransformerName == nil)
170179
valueTransformerName = nil
171-
if valueType != Any.self { self.valueType = valueType }
172-
173180
setOptions(options)
181+
if valueType != Any.self { self.valueType = valueType }
174182
}
175183
}
176184

@@ -196,11 +204,17 @@ private extension NSAttributeDescription {
196204
case .ephemeral: isTransient = true
197205

198206
case .transformableByName(let name):
199-
assert(valueTransformerName == nil)
200-
valueTransformerName = name
207+
fatalError("Not supported")
201208
case .transformableByType(let type):
202209
assert(valueTransformerName == nil)
203-
valueTransformerName = NSStringFromClass(type)
210+
let name = NSStringFromClass(type)
211+
if !ValueTransformer.valueTransformerNames().contains(.init(name)) {
212+
// no access to valueTransformerForName?
213+
let transformer = type.init()
214+
ValueTransformer
215+
.setValueTransformer(transformer, forName: .init(name))
216+
}
217+
valueTransformerName = name
204218

205219
case .allowsCloudEncryption: // FIXME: restrict availability
206220
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {

Tests/ManagedModelTests/SchemaGenerationTests.swift

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ final class SchemaGenerationTests: XCTestCase {
102102
XCTAssertEqual (toAddresses.destination, "Address")
103103
XCTAssertNotNil(toAddresses.destinationEntity)
104104

105-
XCTAssertEqual(address.attributes.count, 2)
105+
XCTAssertEqual(address.attributes.count, 3)
106106
XCTAssertEqual(address.relationships.count, 1)
107107
let toPerson = try XCTUnwrap(address.relationshipsByName["person"])
108108
XCTAssertTrue (toPerson.isRelationship)
@@ -172,7 +172,7 @@ final class SchemaGenerationTests: XCTestCase {
172172
XCTAssertEqual(schema.entitiesByName.count, 2)
173173

174174
let address = try XCTUnwrap(schema.entitiesByName["Address"])
175-
XCTAssertEqual(address.attributes.count, 2)
175+
XCTAssertEqual(address.attributes.count, 3)
176176

177177
let appartment = try XCTUnwrap(address.attributesByName["appartment"])
178178
XCTAssertFalse(appartment.isTransient)
@@ -189,6 +189,34 @@ final class SchemaGenerationTests: XCTestCase {
189189
XCTAssertEqual(street.attributeType, .stringAttributeType)
190190
}
191191

192+
func testRawRepresentableEnum() throws {
193+
let cache = SchemaBuilder()
194+
let schema = NSManagedObjectModel(
195+
[ Fixtures.PersonAddressSchema.Person.self ],
196+
schemaCache: cache
197+
)
198+
199+
XCTAssertEqual(schema.entities.count, 2)
200+
XCTAssertEqual(schema.entitiesByName.count, 2)
201+
202+
let address = try XCTUnwrap(schema.entitiesByName["Address"])
203+
XCTAssertEqual(address.attributes.count, 3)
204+
205+
let street = try XCTUnwrap(address.attributesByName["street"])
206+
XCTAssertFalse(street.isTransient)
207+
XCTAssertFalse(street.isRelationship)
208+
XCTAssertTrue (street.isAttribute)
209+
XCTAssertFalse(street.isOptional)
210+
XCTAssertEqual(street.attributeType, .stringAttributeType)
211+
212+
let type = try XCTUnwrap(address.attributesByName["type"])
213+
XCTAssertFalse(type.isTransient)
214+
XCTAssertFalse(type.isRelationship)
215+
XCTAssertTrue (type.isAttribute)
216+
XCTAssertFalse(type.isOptional)
217+
XCTAssertEqual(type.attributeType, .integer64AttributeType)
218+
}
219+
192220
func testMOM() throws {
193221
let mom = Fixtures.PersonAddressMOM
194222
XCTAssertEqual(mom.entities.count, 2)
@@ -208,7 +236,7 @@ final class SchemaGenerationTests: XCTestCase {
208236
let address = try XCTUnwrap(
209237
entities.first(where: { $0.name == "Address" })
210238
)
211-
XCTAssertEqual(address.attributes.count, 2)
239+
XCTAssertEqual(address.attributes.count, 3)
212240
}
213241

214242
// second run
@@ -221,7 +249,7 @@ final class SchemaGenerationTests: XCTestCase {
221249
let address = try XCTUnwrap(
222250
entities.first(where: { $0.name == "Address" })
223251
)
224-
XCTAssertEqual(address.attributes.count, 2)
252+
XCTAssertEqual(address.attributes.count, 3)
225253
}
226254
}
227255

@@ -235,7 +263,7 @@ final class SchemaGenerationTests: XCTestCase {
235263
let address = try XCTUnwrap(
236264
model1.entities.first(where: { $0.name == "Address" })
237265
)
238-
XCTAssertEqual(address.attributes.count, 2)
266+
XCTAssertEqual(address.attributes.count, 3)
239267
}
240268

241269
// second run
@@ -248,7 +276,7 @@ final class SchemaGenerationTests: XCTestCase {
248276
let address = try XCTUnwrap(
249277
model2.entities.first(where: { $0.name == "Address" })
250278
)
251-
XCTAssertEqual(address.attributes.count, 2)
279+
XCTAssertEqual(address.attributes.count, 3)
252280
}
253281
}
254282

Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,26 @@ extension Fixtures {
2626
var addresses : Set<Address> // [ Address ]
2727
}
2828

29+
enum AddressType: Int {
30+
case home, work
31+
}
32+
2933
@Model
3034
final class Address /*test*/ : NSManagedObject {
3135

3236
var street : String
3337
var appartment : String?
38+
var type : AddressType
3439
var person : Person
3540

3641
// Either: super.init(entity: Self.entity(), insertInto: nil)
3742
// Or: mark this as `convenience`
38-
convenience init(street: String, appartment: String? = nil, person: Person) {
43+
convenience init(street: String, appartment: String? = nil, type: AddressType, person: Person) {
3944
//super.init(entity: Self.entity(), insertInto: nil)
4045
self.init()
4146
self.street = street
4247
self.appartment = appartment
48+
self.type = type
4349
self.person = person
4450
}
4551
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// TransformablePropertySchema.swift
3+
// Created by Adam Kopeć on 11/02/2024.
4+
//
5+
6+
import ManagedModels
7+
8+
extension Fixtures {
9+
// https://github.com/Data-swift/ManagedModels/issues/4
10+
11+
enum TransformablePropertiesSchema: VersionedSchema {
12+
static var models : [ any PersistentModel.Type ] = [
13+
StoredAccess.self
14+
]
15+
16+
public static let versionIdentifier = Schema.Version(0, 1, 0)
17+
18+
@Model
19+
final class StoredAccess: NSManagedObject {
20+
var token : String
21+
var expires : Date
22+
@Attribute(.transformable(by: AccessSIPTransformer.self))
23+
var sip : AccessSIP
24+
@Attribute(.transformable(by: AccessSIPTransformer.self))
25+
var optionalSIP : AccessSIP?
26+
}
27+
28+
class AccessSIP: NSObject {
29+
var username : String
30+
var password : String
31+
32+
init(username: String, password: String) {
33+
self.username = username
34+
self.password = password
35+
}
36+
}
37+
38+
class AccessSIPTransformer: ValueTransformer {
39+
override class func transformedValueClass() -> AnyClass {
40+
return AccessSIP.self
41+
}
42+
43+
override class func allowsReverseTransformation() -> Bool {
44+
return true
45+
}
46+
47+
override func transformedValue(_ value: Any?) -> Any? {
48+
guard let data = value as? Data else { return nil }
49+
guard let array = try? JSONDecoder().decode([String].self, from: data) else { return nil }
50+
return AccessSIP(username: array[0], password: array[1])
51+
}
52+
53+
override func reverseTransformedValue(_ value: Any?) -> Any? {
54+
guard let sip = value as? AccessSIP else { return nil }
55+
return try? JSONEncoder().encode([sip.username, sip.password])
56+
}
57+
}
58+
}
59+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// TransformablePropertiesTests.swift
3+
// Created by Adam Kopeć on 11/02/2024.
4+
//
5+
6+
import XCTest
7+
import Foundation
8+
import CoreData
9+
@testable import ManagedModels
10+
11+
final class TransformablePropertiesTests: XCTestCase {
12+
13+
private let container = try? ModelContainer(
14+
for: Fixtures.TransformablePropertiesSchema.managedObjectModel,
15+
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
16+
)
17+
18+
func testEntityName() throws {
19+
let entityType = Fixtures.TransformablePropertiesSchema.StoredAccess.self
20+
XCTAssertEqual(entityType.entity().name, "StoredAccess")
21+
}
22+
23+
func testPropertySetup() throws {
24+
let valueType = Fixtures.TransformablePropertiesSchema.AccessSIP.self
25+
let attribute = CoreData.NSAttributeDescription(
26+
name: "sip",
27+
options: [.transformable(by: Fixtures.TransformablePropertiesSchema.AccessSIPTransformer.self)],
28+
valueType: valueType,
29+
defaultValue: nil
30+
)
31+
XCTAssertEqual(attribute.name, "sip")
32+
XCTAssertEqual(attribute.attributeType, .transformableAttributeType)
33+
34+
let transformerName = try XCTUnwrap(
35+
ValueTransformer.valueTransformerNames().first(where: {
36+
$0.rawValue.range(of: "AccessSIPTransformer")
37+
!= nil
38+
})
39+
)
40+
let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName))
41+
_ = transformer // to clear unused-wraning
42+
43+
XCTAssertTrue(attribute.valueType ==
44+
NSObject.self)
45+
XCTAssertNotNil(attribute.valueTransformerName)
46+
XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue)
47+
}
48+
49+
func testTransformablePropertyEntity() throws {
50+
let entity = try XCTUnwrap(
51+
container?.managedObjectModel.entitiesByName["StoredAccess"]
52+
)
53+
54+
// Creating the entity should have registered the transformer for the
55+
// CodableBox.
56+
let transformerName = try XCTUnwrap(
57+
ValueTransformer.valueTransformerNames().first(where: {
58+
$0.rawValue.range(of: "AccessSIPTransformer")
59+
!= nil
60+
})
61+
)
62+
let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName))
63+
_ = transformer // to clear unused-wraning
64+
65+
let attribute = try XCTUnwrap(entity.attributesByName["sip"])
66+
XCTAssertEqual(attribute.name, "sip")
67+
XCTAssertTrue(attribute.valueType ==
68+
NSObject.self)
69+
XCTAssertNotNil(attribute.valueTransformerName)
70+
XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue)
71+
}
72+
}

0 commit comments

Comments
 (0)