From ebf4389aef8b39476c171692eac35a20393c751e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 27 Feb 2024 10:13:38 -0800 Subject: [PATCH 1/4] Add support for SwiftData models --- .../CustomDump/Conformances/SwiftData.swift | 31 +++++ .../CustomDump/CustomDumpReflectable.swift | 21 ++- .../Conformances/SwiftDataTests.swift | 126 ++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 Sources/CustomDump/Conformances/SwiftData.swift create mode 100644 Tests/CustomDumpTests/Conformances/SwiftDataTests.swift diff --git a/Sources/CustomDump/Conformances/SwiftData.swift b/Sources/CustomDump/Conformances/SwiftData.swift new file mode 100644 index 00000000..e5c344dc --- /dev/null +++ b/Sources/CustomDump/Conformances/SwiftData.swift @@ -0,0 +1,31 @@ +#if canImport(SwiftData) + import SwiftData + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + struct PersistentModelDump: CustomDumpReflectable { + let wrappedValue: T + + var customDumpMirror: Mirror { + Mirror( + self.wrappedValue, + children: T.schemaMetadata.map { propertyMetadata in + let propertyMetadata = PropertyMetadata(propertyMetadata) + return (propertyMetadata.name, self.wrappedValue[keyPath: propertyMetadata.keyPath]) + }, + displayStyle: .class + ) + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + private struct PropertyMetadata { + let name: String + let keyPath: PartialKeyPath + + init(_ propertyMetadata: Schema.PropertyMetadata) { + let children = Mirror(reflecting: propertyMetadata).children.makeIterator() + self.name = children.next()!.value as! String + self.keyPath = children.next()!.value as! PartialKeyPath + } + } +#endif diff --git a/Sources/CustomDump/CustomDumpReflectable.swift b/Sources/CustomDump/CustomDumpReflectable.swift index 8e982361..af135ea7 100644 --- a/Sources/CustomDump/CustomDumpReflectable.swift +++ b/Sources/CustomDump/CustomDumpReflectable.swift @@ -1,3 +1,7 @@ +#if canImport(SwiftData) + import SwiftData +#endif + /// A type that explicitly supplies its own mirror for ``customDump(_:to:name:indent:maxDepth:)`` /// and ``diff(_:_:format:)``. /// @@ -126,6 +130,21 @@ public protocol CustomDumpReflectable { extension Mirror { init(customDumpReflecting subject: Any) { - self = (subject as? CustomDumpReflectable)?.customDumpMirror ?? Mirror(reflecting: subject) + if let subject = subject as? CustomDumpReflectable { + self = subject.customDumpMirror + return + } + #if canImport(SwiftData) + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + let subject = subject as? any PersistentModel + { + func open(_ model: T) -> Mirror { + PersistentModelDump(wrappedValue: model).customDumpMirror + } + self = open(subject) + return + } + #endif + self = Mirror(reflecting: subject) } } diff --git a/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift b/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift new file mode 100644 index 00000000..1215f2a4 --- /dev/null +++ b/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift @@ -0,0 +1,126 @@ +#if canImport(SwiftData) + import CustomDump + import SwiftData + import XCTest + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + final class SwiftDataTests: XCTestCase { + func testModel() throws { + let schema = Schema([BucketListItem.self, LivingAccommodation.self, Trip.self]) + let configuration = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try ModelContainer(for: schema, configurations: [configuration]) + let context = ModelContext(container) + + let trip = Trip( + name: "Outer Borough Trip #1", + destination: "Brooklyn, NY", + startDate: Date(timeIntervalSinceReferenceDate: 0), + endDate: Date(timeIntervalSinceReferenceDate: 60 * 60 * 24 * 7) + ) + context.insert(trip) + + let bucketListItem = BucketListItem( + title: "Brooklyn Bridge Park", + details: """ + Explore the sweeping vistas, rich ecology, expansive piers, and vibrant programming of \ + this special waterfront park + """, + hasReservation: false, + isInPlan: false + ) + context.insert(bucketListItem) + trip.bucketList.append(bucketListItem) + + let livingAccommodation = LivingAccommodation( + address: """ + 60 Furman St + Brooklyn, NY 11201 + """, + placeName: "1 Hotel Brooklyn Bridge" + ) + context.insert(livingAccommodation) + trip.livingAccommodation = livingAccommodation + + XCTAssertNoDifference( + String(customDumping: trip), + #""" + Trip( + destination: "Brooklyn, NY", + endDate: Date(2001-01-08T00:00:00.000Z), + name: "Outer Borough Trip #1", + startDate: Date(2001-01-01T00:00:00.000Z), + bucketList: [ + [0]: BucketListItem( + details: "Explore the sweeping vistas, rich ecology, expansive piers, and vibrant programming of this special waterfront park", + hasReservation: false, + isInPlan: false, + title: "Brooklyn Bridge Park", + trip: Trip(↩︎) + ) + ], + livingAccommodation: LivingAccommodation( + address: """ + 60 Furman St + Brooklyn, NY 11201 + """, + placeName: "1 Hotel Brooklyn Bridge", + trip: Trip(↩︎) + ) + ) + """# + ) + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Model final class BucketListItem { + var details: String + var hasReservation: Bool + var isInPlan: Bool + var title: String + var trip: Trip? + + init(title: String, details: String, hasReservation: Bool, isInPlan: Bool) { + self.title = title + self.details = details + self.hasReservation = hasReservation + self.isInPlan = isInPlan + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Model final class LivingAccommodation { + var address: String + var placeName: String + var trip: Trip? + + init(address: String, placeName: String) { + self.address = address + self.placeName = placeName + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Model final class Trip { + var destination: String + var endDate: Date + var name: String + var startDate: Date + + @Relationship(deleteRule: .cascade, inverse: \BucketListItem.trip) + var bucketList: [BucketListItem] = [BucketListItem]() + + @Relationship(deleteRule: .cascade, inverse: \LivingAccommodation.trip) + var livingAccommodation: LivingAccommodation? + + init( + name: String, destination: String, + startDate: Date = .now, endDate: Date = .distantFuture + ) { + self.name = name + self.destination = destination + self.startDate = startDate + self.endDate = endDate + } + } +#endif From acd5ab954f8af969c828fb23d7884b04fdfebd5c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 27 Feb 2024 11:03:05 -0800 Subject: [PATCH 2/4] Update SwiftData.swift --- Sources/CustomDump/Conformances/SwiftData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CustomDump/Conformances/SwiftData.swift b/Sources/CustomDump/Conformances/SwiftData.swift index e5c344dc..61e7c65b 100644 --- a/Sources/CustomDump/Conformances/SwiftData.swift +++ b/Sources/CustomDump/Conformances/SwiftData.swift @@ -1,4 +1,4 @@ -#if canImport(SwiftData) +#if swift(>=5.9) && canImport(SwiftData) import SwiftData @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) From 08d7b33ec54ea21bdf9b40d0ead87f920f180a21 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 27 Feb 2024 11:03:21 -0800 Subject: [PATCH 3/4] Update CustomDumpReflectable.swift --- Sources/CustomDump/CustomDumpReflectable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CustomDump/CustomDumpReflectable.swift b/Sources/CustomDump/CustomDumpReflectable.swift index af135ea7..83da2b11 100644 --- a/Sources/CustomDump/CustomDumpReflectable.swift +++ b/Sources/CustomDump/CustomDumpReflectable.swift @@ -1,4 +1,4 @@ -#if canImport(SwiftData) +#if swift(>=5.9) && canImport(SwiftData) import SwiftData #endif @@ -134,7 +134,7 @@ extension Mirror { self = subject.customDumpMirror return } - #if canImport(SwiftData) + #if swift(>=5.9) && canImport(SwiftData) if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any PersistentModel { From e803a5a2ec4bed0656d9835529f0b4a4ab2aeb84 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 27 Feb 2024 11:03:41 -0800 Subject: [PATCH 4/4] Update SwiftDataTests.swift --- Tests/CustomDumpTests/Conformances/SwiftDataTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift b/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift index 1215f2a4..da22d948 100644 --- a/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift +++ b/Tests/CustomDumpTests/Conformances/SwiftDataTests.swift @@ -1,4 +1,4 @@ -#if canImport(SwiftData) +#if swift(>=5.9) && canImport(SwiftData) import CustomDump import SwiftData import XCTest