Skip to content

Commit e80bfee

Browse files
Added Indexed property wrapper
1 parent 0d14880 commit e80bfee

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// Indexed.swift
3+
// CodableDatastore
4+
//
5+
// Created by Dimitri Bouniol on 2023-05-31.
6+
// Copyright © 2023 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// An alias representing the requirements for a property to be indexable, namely that they conform to both ``/Swift/Codable`` and ``/Swift/Comparable``.
12+
public typealias Indexable = Comparable & Codable
13+
14+
/// A property wrapper to mark a property as one that is indexable by a data store.
15+
///
16+
/// Indexable properties must be ``/Swift/Codable`` so that their values can be encoded and decoded,
17+
/// and be ``/Swift/Comparable`` so that a stable order may be formed when saving to a data store.
18+
///
19+
/// To mark a property as one that an index should be built against, mark it as such:
20+
///
21+
/// struct MyStruct {
22+
/// var id: UUID
23+
///
24+
/// @Indexed
25+
/// var name: String
26+
///
27+
/// @Indexed
28+
/// var age: Int = 1
29+
///
30+
/// var other: [Int] = []
31+
///
32+
/// //@Indexed
33+
/// //var nonCodable = NonCodable() // Not allowed!
34+
///
35+
/// //@Indexed
36+
/// //var nonComparable = NonComparable() // Not allowed!
37+
/// }
38+
///
39+
/// - Note: The `id` field from ``/Foundation/Identifiable`` does not need to be indexed, as it is indexed by default for instance uniqueness in a data store.
40+
///
41+
/// - Warning: Although changing which properties are indexed, including their names and types, is fully supported,
42+
/// changing the ``/Swift/Comparable`` implementation of a type between builds can lead to problems. If ``/Swift/Comparable``
43+
/// conformance changes, you should declare a new version along side it so you can force an index to be migrated at the same time.
44+
///
45+
/// > Attention:
46+
/// > Only use this type as a property wrapper. Marking a computed property as returning an ``Indexed`` field is not supported, and will fail at runtime.
47+
/// >
48+
/// > This is because the index won't be properly detected when warming up a datastore and won't properly
49+
/// migrate indices as a result of that failed detection:
50+
/// >
51+
/// >```
52+
/// >struct MyStruct {
53+
/// > var id: UUID
54+
/// >
55+
/// > @Indexed
56+
/// > var name: String
57+
/// >
58+
/// > @Indexed
59+
/// > var age: Int = 1
60+
/// >
61+
/// > /// Don't do this:
62+
/// > var composed: Indexed<String> { Indexed(wrappedValue: "\(name) \(age)") }
63+
/// >}
64+
/// >```
65+
///
66+
@propertyWrapper
67+
public struct Indexed<T> where T: Indexable {
68+
/// The underlying value that the index will be based off of.
69+
///
70+
/// This is ordinarily handled transparently when used as a property wrapper.
71+
public var wrappedValue: T
72+
73+
/// Initialize an ``Indexed`` value with an initial value.
74+
///
75+
/// This is ordinarily handled transparently when used as a property wrapper.
76+
public init(wrappedValue: T) {
77+
self.wrappedValue = wrappedValue
78+
}
79+
80+
/// The projected value of the indexed property, which is ourself.
81+
///
82+
/// This allows the indexed property to be used in the data store using `.$property` syntax.
83+
public var projectedValue: Self { self }
84+
}
85+
86+
/// An internal protocol to use when evaluating types for indexed properties.
87+
protocol _Indexed {
88+
associatedtype T: Indexable
89+
90+
init(wrappedValue: T)
91+
92+
var wrappedValue: T { get }
93+
var projectedValue: Self { get }
94+
}
95+
extension Indexed: _Indexed {}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// IndexedTests.swift
3+
// CodableDatastore
4+
//
5+
// Created by Dimitri Bouniol on 2023-05-31.
6+
// Copyright © 2023 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import XCTest
10+
@testable import CodableDatastore
11+
12+
final class IndexedTests: XCTestCase {
13+
func testIndexed() throws {
14+
struct NonCodable {}
15+
16+
struct TestStruct {
17+
var id: UUID
18+
19+
@Indexed
20+
var name: String
21+
22+
@Indexed
23+
var age: Int = 1
24+
25+
var other: [Int] = []
26+
27+
// @Indexed
28+
// var nonCodable = NonCodable() // Not allowed!
29+
30+
// Technically possible, but heavily discouraged:
31+
var composed: Indexed<String> { Indexed(wrappedValue: "\(name) \(age)") }
32+
}
33+
34+
let myValue = TestStruct(id: UUID(), name: "Hello!")
35+
36+
XCTAssertEqual("\(myValue[keyPath: \.age])", "1")
37+
XCTAssertEqual("\(myValue[keyPath: \.$age])", "Indexed<Int>(wrappedValue: 1)")
38+
XCTAssertEqual("\(myValue[keyPath: \.composed])", "Indexed<String>(wrappedValue: \"Hello! 1\")")
39+
40+
// This did not work unfortunately:
41+
// withUnsafeTemporaryAllocation(of: TestStruct.self, capacity: 1) { pointer in
42+
//// print(Mirror(reflecting: pointer).children)
43+
// let value = pointer.first!
44+
//
45+
// let mirror = Mirror(reflecting: value)
46+
// var indexedProperties: [String] = []
47+
// for child in mirror.children {
48+
// guard let label = child.label else { continue }
49+
// let childType = type(of: child.value)
50+
// guard childType is _Indexed.Type else { continue }
51+
// print("Child: \(label), type: \(childType)")
52+
// indexedProperties.append(label)
53+
// }
54+
// print("Indexable Children from type: \(indexedProperties)")
55+
// }
56+
57+
58+
// let mirror = Mirror(reflecting: TestStruct.self) // Doesn't work :(
59+
let mirror = Mirror(reflecting: myValue)
60+
var indexedProperties: [String] = []
61+
for child in mirror.children {
62+
guard let label = child.label else { continue }
63+
let childType = type(of: child.value)
64+
guard childType is any _Indexed.Type else { continue }
65+
indexedProperties.append(label)
66+
}
67+
XCTAssertEqual(indexedProperties, ["_name", "_age"])
68+
69+
struct TestAccessor<T> {
70+
func load<V>(from keypath: KeyPath<T, Indexed<V>>) -> [T] {
71+
XCTAssertEqual(keypath, \TestStruct.$age)
72+
return []
73+
}
74+
}
75+
76+
let accessor: TestAccessor<TestStruct> = TestAccessor()
77+
// let values = accessor.load(from: \.other) // not allowed!
78+
// let values = accessor.load(from: \.age) // not allowed!
79+
let values = accessor.load(from: \.$age)
80+
XCTAssertEqual("\(values)", "[]")
81+
XCTAssertEqual("\(type(of: values))", "Array<TestStruct>")
82+
}
83+
}

0 commit comments

Comments
 (0)