Skip to content

Commit 04cd1f9

Browse files
Updated IndexPath to surface underlying indexed type
1 parent 88bb826 commit 04cd1f9

File tree

6 files changed

+77
-48
lines changed

6 files changed

+77
-48
lines changed

Sources/CodableDatastore/Datastore/Datastore.swift

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public actor Datastore<
2020
let version: Version
2121
let encoder: (_ instance: CodedType) async throws -> Data
2222
let decoders: [Version: (_ data: Data) async throws -> CodedType]
23-
let directIndexes: [IndexPath<CodedType>]
24-
let computedIndexes: [IndexPath<CodedType>]
23+
let directIndexes: [IndexPath<CodedType, _AnyIndexed>]
24+
let computedIndexes: [IndexPath<CodedType, _AnyIndexed>]
2525

2626
var updatedDescriptor: DatastoreDescriptor?
2727

@@ -31,8 +31,8 @@ public actor Datastore<
3131
fileprivate var storeMigrationStatus: TaskStatus = .waiting
3232
fileprivate var storeMigrationProgressHandlers: [ProgressHandler] = []
3333

34-
fileprivate var indexMigrationStatus: [IndexPath<CodedType> : TaskStatus] = [:]
35-
fileprivate var indexMigrationProgressHandlers: [IndexPath<CodedType> : ProgressHandler] = [:]
34+
fileprivate var indexMigrationStatus: [IndexPath<CodedType, _AnyIndexed> : TaskStatus] = [:]
35+
fileprivate var indexMigrationProgressHandlers: [IndexPath<CodedType, _AnyIndexed> : ProgressHandler] = [:]
3636

3737
public init(
3838
persistence: some Persistence<AccessMode>,
@@ -42,8 +42,8 @@ public actor Datastore<
4242
identifierType: IdentifierType.Type,
4343
encoder: @escaping (_ instance: CodedType) async throws -> Data,
4444
decoders: [Version: (_ data: Data) async throws -> CodedType],
45-
directIndexes: [IndexPath<CodedType>] = [],
46-
computedIndexes: [IndexPath<CodedType>] = [],
45+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
46+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
4747
configuration: Configuration = .init()
4848
) where AccessMode == ReadWrite {
4949
self.persistence = persistence
@@ -62,8 +62,8 @@ public actor Datastore<
6262
codedType: CodedType.Type = CodedType.self,
6363
identifierType: IdentifierType.Type,
6464
decoders: [Version: (_ data: Data) async throws -> CodedType],
65-
directIndexes: [IndexPath<CodedType>] = [],
66-
computedIndexes: [IndexPath<CodedType>] = [],
65+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
66+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
6767
configuration: Configuration = .init()
6868
) where AccessMode == ReadOnly {
6969
self.persistence = persistence
@@ -172,7 +172,7 @@ extension Datastore where AccessMode == ReadWrite {
172172
/// - index: The index to migrate.
173173
/// - minimumVersion: The minimum valid version for an index to not be migrated.
174174
/// - progressHandler: A closure that will be regularly called with progress during the migration. If no migration needs to occur, it won't be called, so setup and tear down any UI within the handler.
175-
public func migrate(index: IndexPath<CodedType>, ifLessThan minimumVersion: Version, progressHandler: ProgressHandler? = nil) async throws {
175+
public func migrate(index: IndexPath<CodedType, _AnyIndexed>, ifLessThan minimumVersion: Version, progressHandler: ProgressHandler? = nil) async throws {
176176
try await persistence._withTransaction(options: []) { transaction in
177177
guard
178178
/// If we have no descriptor, then no data exists to be migrated.
@@ -214,7 +214,7 @@ extension Datastore where AccessMode == ReadWrite {
214214
}
215215
}
216216

217-
func migrate(index: IndexPath<CodedType>, progressHandler: ProgressHandler? = nil) async throws {
217+
func migrate(index: IndexPath<CodedType, _AnyIndexed>, progressHandler: ProgressHandler? = nil) async throws {
218218
// TODO: Migrate just that index, use indexMigrationStatus and indexMigrationProgressHandlers to record progress.
219219
}
220220

@@ -308,7 +308,7 @@ extension Datastore {
308308
public nonisolated func load<IndexedValue: Indexable>(
309309
_ range: some IndexRangeExpression<IndexedValue>,
310310
order: RangeOrder = .ascending,
311-
from indexPath: IndexPath<CodedType>
311+
from indexPath: IndexPath<CodedType, _SomeIndexed<IndexedValue>>
312312
) -> some TypedAsyncSequence<CodedType> {
313313
let a: AsyncThrowingBackpressureStream<CodedType> = AsyncThrowingBackpressureStream { provider in
314314
try await self.warmupIfNeeded()
@@ -353,23 +353,23 @@ extension Datastore {
353353
public nonisolated func load<IndexedValue: Indexable>(
354354
_ range: IndexRange<IndexedValue>,
355355
order: RangeOrder = .ascending,
356-
from keypath: IndexPath<CodedType>
356+
from keypath: IndexPath<CodedType, _SomeIndexed<IndexedValue>>
357357
) -> some TypedAsyncSequence<CodedType> {
358358
load(range, order: order, from: keypath)
359359
}
360360

361-
public nonisolated func load(
361+
public nonisolated func load<IndexedValue: Indexable>(
362362
_ range: Swift.UnboundedRange,
363363
order: RangeOrder = .ascending,
364-
from keypath: IndexPath<CodedType>
364+
from keypath: IndexPath<CodedType, _SomeIndexed<IndexedValue>>
365365
) -> some TypedAsyncSequence<CodedType> {
366-
load(IndexRange<Int>(), order: order, from: keypath)
366+
load(IndexRange<IndexedValue>(), order: order, from: keypath)
367367
}
368368

369369
public nonisolated func load<IndexedValue: Indexable>(
370370
_ value: IndexedValue,
371371
order: RangeOrder = .ascending,
372-
from keypath: IndexPath<CodedType>
372+
from keypath: IndexPath<CodedType, _SomeIndexed<IndexedValue>>
373373
) -> some TypedAsyncSequence<CodedType> {
374374
load(value...value, order: order, from: keypath)
375375
}
@@ -781,8 +781,8 @@ extension Datastore where AccessMode == ReadWrite {
781781
encoder: JSONEncoder = JSONEncoder(),
782782
decoder: JSONDecoder = JSONDecoder(),
783783
migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> CodedType],
784-
directIndexes: [IndexPath<CodedType>] = [],
785-
computedIndexes: [IndexPath<CodedType>] = [],
784+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
785+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
786786
configuration: Configuration = .init()
787787
) -> Self {
788788
self.init(
@@ -811,8 +811,8 @@ extension Datastore where AccessMode == ReadWrite {
811811
identifierType: IdentifierType.Type,
812812
outputFormat: PropertyListSerialization.PropertyListFormat = .binary,
813813
migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> CodedType],
814-
directIndexes: [IndexPath<CodedType>] = [],
815-
computedIndexes: [IndexPath<CodedType>] = [],
814+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
815+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
816816
configuration: Configuration = .init()
817817
) -> Self {
818818
let encoder = PropertyListEncoder()
@@ -848,8 +848,8 @@ extension Datastore where AccessMode == ReadOnly {
848848
identifierType: IdentifierType.Type,
849849
decoder: JSONDecoder = JSONDecoder(),
850850
migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> CodedType],
851-
directIndexes: [IndexPath<CodedType>] = [],
852-
computedIndexes: [IndexPath<CodedType>] = [],
851+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
852+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
853853
configuration: Configuration = .init()
854854
) -> Self {
855855
self.init(
@@ -876,8 +876,8 @@ extension Datastore where AccessMode == ReadOnly {
876876
codedType: CodedType.Type = CodedType.self,
877877
identifierType: IdentifierType.Type,
878878
migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> CodedType],
879-
directIndexes: [IndexPath<CodedType>] = [],
880-
computedIndexes: [IndexPath<CodedType>] = [],
879+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
880+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
881881
configuration: Configuration = .init()
882882
) -> Self {
883883
let decoder = PropertyListDecoder()
@@ -910,8 +910,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
910910
codedType: CodedType.Type = CodedType.self,
911911
encoder: @escaping (_ object: CodedType) async throws -> Data,
912912
decoders: [Version: (_ data: Data) async throws -> CodedType],
913-
directIndexes: [IndexPath<CodedType>] = [],
914-
computedIndexes: [IndexPath<CodedType>] = [],
913+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
914+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
915915
configuration: Configuration = .init()
916916
) {
917917
self.init(
@@ -936,8 +936,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
936936
encoder: JSONEncoder = JSONEncoder(),
937937
decoder: JSONDecoder = JSONDecoder(),
938938
migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> CodedType],
939-
directIndexes: [IndexPath<CodedType>] = [],
940-
computedIndexes: [IndexPath<CodedType>] = [],
939+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
940+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
941941
configuration: Configuration = .init()
942942
) -> Self {
943943
self.JSONStore(
@@ -962,8 +962,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
962962
codedType: CodedType.Type = CodedType.self,
963963
outputFormat: PropertyListSerialization.PropertyListFormat = .binary,
964964
migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> CodedType],
965-
directIndexes: [IndexPath<CodedType>] = [],
966-
computedIndexes: [IndexPath<CodedType>] = [],
965+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
966+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
967967
configuration: Configuration = .init()
968968
) -> Self {
969969
self.propertyListStore(
@@ -988,8 +988,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
988988
version: Version,
989989
codedType: CodedType.Type = CodedType.self,
990990
decoders: [Version: (_ data: Data) async throws -> CodedType],
991-
directIndexes: [IndexPath<CodedType>] = [],
992-
computedIndexes: [IndexPath<CodedType>] = [],
991+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
992+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
993993
configuration: Configuration = .init()
994994
) {
995995
self.init(
@@ -1012,8 +1012,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
10121012
codedType: CodedType.Type = CodedType.self,
10131013
decoder: JSONDecoder = JSONDecoder(),
10141014
migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> CodedType],
1015-
directIndexes: [IndexPath<CodedType>] = [],
1016-
computedIndexes: [IndexPath<CodedType>] = [],
1015+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
1016+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
10171017
configuration: Configuration = .init()
10181018
) -> Self {
10191019
self.readOnlyJSONStore(
@@ -1036,8 +1036,8 @@ extension Datastore where CodedType: Identifiable, IdentifierType == CodedType.I
10361036
version: Version,
10371037
codedType: CodedType.Type = CodedType.self,
10381038
migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> CodedType],
1039-
directIndexes: [IndexPath<CodedType>] = [],
1040-
computedIndexes: [IndexPath<CodedType>] = [],
1039+
directIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
1040+
computedIndexes: [IndexPath<CodedType, _AnyIndexed>] = [],
10411041
configuration: Configuration = .init()
10421042
) -> Self {
10431043
self.readOnlyPropertyListStore(

Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ extension DatastoreDescriptor {
9090
version: Version,
9191
sampleInstance: CodedType,
9292
identifierType: IdentifierType.Type,
93-
directIndexes directIndexPaths: [IndexPath<CodedType>],
94-
computedIndexes computedIndexPaths: [IndexPath<CodedType>]
93+
directIndexes directIndexPaths: [IndexPath<CodedType, _AnyIndexed>],
94+
computedIndexes computedIndexPaths: [IndexPath<CodedType, _AnyIndexed>]
9595
) throws where Version.RawValue: Indexable {
9696
let versionData = try Data(version)
9797

@@ -163,7 +163,7 @@ extension DatastoreDescriptor.IndexDescriptor {
163163
init<CodedType: Codable>(
164164
version: Data,
165165
sampleInstance: CodedType,
166-
indexPath: IndexPath<CodedType>
166+
indexPath: IndexPath<CodedType, _AnyIndexed>
167167
) {
168168
let sampleIndexValue = sampleInstance[keyPath: indexPath]
169169
let indexType = sampleIndexValue.indexedType

Sources/CodableDatastore/Indexes/IndexPath.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
//
88

99
/// A keypath with an associated name.
10-
public struct IndexPath<Root>: Equatable, Hashable {
10+
public struct IndexPath<Root, Value: _AnyIndexed>: Equatable, Hashable {
1111
/// The ``/Swift/KeyPath`` associated with the index path.
12-
public let keyPath: KeyPath<Root, _AnyIndexed>
12+
public let keyPath: KeyPath<Root, Value>
1313

1414
/// The path as a string.
1515
public let path: String
@@ -20,10 +20,21 @@ public struct IndexPath<Root>: Equatable, Hashable {
2020
/// - Parameters:
2121
/// - uncheckedKeyPath: The keypath to bind to.
2222
/// - path: The name of the path as a string, which should match the keypath itself.
23-
public init(uncheckedKeyPath: KeyPath<Root, _AnyIndexed>, path: String) {
23+
public init(uncheckedKeyPath: KeyPath<Root, Value>, path: String) {
2424
self.keyPath = uncheckedKeyPath
2525
self.path = path
2626
}
27+
28+
/// Initialize a new ``IndexPath``, erasing its type in the process.
29+
///
30+
/// - Note: It is preferable to use the #indexPath macro instead, as it will infer the path automatically.
31+
/// - Parameters:
32+
/// - uncheckedKeyPath: The keypath to bind to.
33+
/// - path: The name of the path as a string, which should match the keypath itself.
34+
public init<IndexedType: Indexable>(uncheckedKeyPath: KeyPath<Root, _SomeIndexed<IndexedType>>, path: String) where Value == _AnyIndexed {
35+
self.keyPath = uncheckedKeyPath.appending(path: \.anyIndexed)
36+
self.path = path
37+
}
2738
}
2839

2940
extension IndexPath: Comparable {
@@ -33,7 +44,11 @@ extension IndexPath: Comparable {
3344
}
3445

3546
extension Encodable {
36-
subscript(keyPath indexPath: IndexPath<Self>) -> _AnyIndexed {
47+
subscript(keyPath indexPath: IndexPath<Self, _AnyIndexed>) -> _AnyIndexed {
48+
return self[keyPath: indexPath.keyPath]
49+
}
50+
51+
subscript<Value>(keyPath indexPath: IndexPath<Self, _SomeIndexed<Value>>) -> _SomeIndexed<Value> {
3752
return self[keyPath: indexPath.keyPath]
3853
}
3954
}

Sources/CodableDatastore/Indexes/Indexed.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,41 @@ public struct Indexed<T> where T: Indexable {
8080
/// The projected value of the indexed property, which is a type-erased version of ourself.
8181
///
8282
/// This allows the indexed property to be used in the data store using `.$property` syntax.
83-
public var projectedValue: _AnyIndexed { _AnyIndexed(indexed: self) }
83+
public var projectedValue: _SomeIndexed<T> { _SomeIndexed(indexed: self) }
8484
}
8585

8686
/// A type-erased wrapper for indexed types.
8787
///
8888
/// You should not reach for this directly, and instead use the @``Indexed`` property wrapper.
89-
public struct _AnyIndexed {
89+
public class _AnyIndexed {
9090
var indexed: any _IndexedProtocol
9191
var indexedType: String
9292

9393
init<T>(indexed: Indexed<T>) {
9494
self.indexed = indexed
9595
indexedType = String(describing: T.self)
9696
}
97+
98+
var anyIndexed: _AnyIndexed {
99+
self
100+
}
101+
}
102+
103+
public class _SomeIndexed<Value: Indexable>: _AnyIndexed {
104+
init(indexed: Indexed<Value>) {
105+
super.init(indexed: indexed)
106+
}
97107
}
98108

99109
/// An internal protocol to use when evaluating types for indexed properties.
100110
protocol _IndexedProtocol<T>: Indexable {
101111
associatedtype T: Indexable
112+
associatedtype ProjectedType: _AnyIndexed
102113

103114
init(wrappedValue: T)
104115

105116
var wrappedValue: T { get }
106-
var projectedValue: _AnyIndexed { get }
117+
var projectedValue: ProjectedType { get }
107118
}
108119

109120
extension _IndexedProtocol {

Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ final class DiskPersistenceDatastoreTests: XCTestCase {
8080

8181
let count = try await datastore.count
8282
XCTAssertEqual(count, 3)
83+
84+
let values = try await datastore.load("A"..."Z", order: .descending, from: IndexPath(uncheckedKeyPath: \.$value, path: "$value")).map { $0.id }.reduce(into: []) { $0.append($1) }
85+
XCTAssertEqual(values, ["2", "3", "1"])
8386
}
8487

8588
func testObservingEntries() async throws {

Tests/CodableDatastoreTests/IndexedTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final class IndexedTests: XCTestCase {
3535

3636
XCTAssertEqual("\(myValue[keyPath: \.age])", "1")
3737
// XCTAssertEqual("\(myValue[keyPath: \.$age])", "Indexed<Int>(wrappedValue: 1)")
38-
XCTAssertEqual("\(myValue[keyPath: \.composed])", #"_AnyIndexed(indexed: CodableDatastore.Indexed<Swift.String>(wrappedValue: "Hello! 1"), indexedType: "String")"#)
38+
XCTAssertEqual("\(myValue[keyPath: \.composed])", #"CodableDatastore._SomeIndexed<Swift.String>"#)
3939

4040
// This did not work unfortunately:
4141
// withUnsafeTemporaryAllocation(of: TestStruct.self, capacity: 1) { pointer in
@@ -67,7 +67,7 @@ final class IndexedTests: XCTestCase {
6767
XCTAssertEqual(indexedProperties, ["_name", "_age"])
6868

6969
struct TestAccessor<T> {
70-
func load(from keypath: KeyPath<T, _AnyIndexed>) -> [T] {
70+
func load<V: _AnyIndexed>(from keypath: KeyPath<T, V>) -> [T] {
7171
XCTAssertEqual(keypath, \TestStruct.$age)
7272
return []
7373
}

0 commit comments

Comments
 (0)