Skip to content
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

feat(collection): [#29] Allow for any kind of Index #40

Merged
merged 2 commits into from
May 14, 2023
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
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 127,7 @@ identityMap.find(Book.self, id: 1)?

### Relational objects

To store objects containing other objects you need to make them conform to one protocol: `Aggregate`.
To store objects containing nested identity objects you need to make them conform to one protocol: `Aggregate`.

```swift
struct AuthorBooks: Aggregate {
Expand Down Expand Up @@ -167,7 167,7 @@ identityMap.find(Book.self, id: "ACK") // A Clash of Kings
identityMap.find(Book.self, id: "ADD") // A Dance with Dragons
```

You can also modify any of them however you want:
You can also modify any of them however you want. Notice the change is visible from the object itself AND from aggregate objects:

```swift
let newAuthor = Author(id: 1, name: "George R.R MartinI")
Expand All @@ -178,7 178,7 @@ identityMap.find(Author.self, id: 1) // George R.R MartinI
identityMap.find(AuthorBooks.self, id: 1) // George R.R MartinI [A Clash of Kings, A Dance with Dragons]
```

> You might think about storing books on `Author` directly (`author.books`). In this case `Author` would need to implement `Aggregate` and declare `books` are nested entity.
> You might think about storing books on `Author` directly (`author.books`). In this case `Author` needs to implement `Aggregate` and declare `books` as nested entity.
>
> However I strongly advise you to not nest `Identifiable` objects into other `Identifiable` objects. Read [Handling relationships](https://swiftunwrap.com/article/modeling-done-right/) article if you want to know more about this subject.

Expand Down Expand Up @@ -269,10 269,6 @@ identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings" because canc

## Known limitations

### Custom collections are not supported

Custom collections are actually supported but for now you need to import `Accelerate` and conform to `AccelerateMutableBuffer`. Hopefully this restriction will be lifted.

### Associated value enums require double update

Let's say you have an enum with `Identifiable`/`Aggregate`:
Expand Down
4 changes: 2 additions & 2 deletions Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 45,7 @@ public struct PartialIdentifiableKeyPath<Root> {
}
}

public init<C: MutableCollection>(_ keyPath: KeyPath<Root, C>) where C.Element: Identifiable, C.Index == Int {
public init<C: MutableCollection>(_ keyPath: KeyPath<Root, C>) where C.Element: Identifiable, C.Index: Hashable {
self.keyPath = keyPath
self.accept = { parent, root, stamp, visitor in
visitor.visit(
Expand All @@ -55,7 55,7 @@ public struct PartialIdentifiableKeyPath<Root> {
}
}

public init<C: MutableCollection>(_ keyPath: KeyPath<Root, C>) where C.Element: Aggregate, C.Index == Int {
public init<C: MutableCollection>(_ keyPath: KeyPath<Root, C>) where C.Element: Aggregate, C.Index: Hashable {
self.keyPath = keyPath
self.accept = { parent, root, stamp, visitor in
visitor.visit(
Expand Down
22 changes: 13 additions & 9 deletions Sources/CohesionKit/KeyPath/UnsafeMutablePointer KeyPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 4,24 @@ extension UnsafeMutablePointer {

func assign<Value>(_ value: Value, to keyPath: KeyPath<Pointee, Value>) {

guard let unsafePointer = UnsafeMutablePointer<Value>(mutating: pointer(to: keyPath)) else {
fatalError("cannot update value for KeyPath<\(Pointee.self), \(Value.self)>. Computed properties are not supported.")
guard let unsafeValuePointer = UnsafeMutablePointer<Value>(mutating: pointer(to: keyPath)) else {
fatalError("cannot update value for KeyPath<\(Pointee.self), \(Value.self)>: failed to access memory pointer.")
}

unsafePointer.pointee = value
unsafeValuePointer.pointee = value
}

func assign<C: MutableCollection>(_ value: C.Element, to keyPath: KeyPath<Pointee, C>, index: C.Index)
where C.Index == Int {

guard let unsafePointer = UnsafeMutablePointer<C>(mutating: pointer(to: keyPath)) else {
fatalError("cannot update value for KeyPath<\(Pointee.self), \(C.self)>. Computed properties are not supported.")
func assign<C: MutableCollection>(_ value: C.Element, to keyPath: KeyPath<Pointee, C>, index: C.Index) {

guard let unsafeCollectionPointer = UnsafeMutablePointer<C>(mutating: pointer(to: keyPath)) else {
fatalError("cannot update value for KeyPath<\(Pointee.self), \(C.self)>: failed to access memory pointer.")
}

unsafePointer.pointee.withContiguousMutableStorageIfAvailable { $0[index] = value }
/// calculate the distance in memory where the object is located to update it
let distance = unsafeCollectionPointer
.pointee
.distance(from: unsafeCollectionPointer.pointee.startIndex, to: index)

unsafeCollectionPointer.pointee.withContiguousMutableStorageIfAvailable { $0[distance] = value }
}
}
2 changes: 1 addition & 1 deletion Sources/CohesionKit/Storage/EntityNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 61,7 @@ class EntityNode<T>: AnyEntityNode {

/// observe one of the node child whose type is a collection
func observeChild<C: MutableCollection>(_ childNode: EntityNode<C.Element>, for keyPath: KeyPath<T, C>, index: C.Index)
where C.Index == Int {
where C.Index: Hashable {
observeChild(childNode, identity: keyPath.appending(path: \C[index])) { pointer, newValue in
pointer.assign(newValue, to: keyPath, index: index)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 33,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor {
}

func visit<Root, C: MutableCollection>(context: EntityContext<Root, C>, entities: C)
where C.Element: Identifiable, C.Index == Int {
where C.Element: Identifiable, C.Index: Hashable {

for index in entities.indices {
context.parent.observeChild(
Expand All @@ -45,7 45,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor {
}

func visit<Root, C: MutableCollection>(context: EntityContext<Root, C>, entities: C)
where C.Element: Aggregate, C.Index == Int {
where C.Element: Aggregate, C.Index: Hashable {

for index in entities.indices {
context.parent.observeChild(
Expand Down
4 changes: 2 additions & 2 deletions Sources/CohesionKit/Visitor/NestedEntitiesVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 9,8 @@ protocol NestedEntitiesVisitor {
func visit<Root, T: Aggregate>(context: EntityContext<Root, T?>, entity: T?)

func visit<Root, C: MutableCollection>(context: EntityContext<Root, C>, entities: C)
where C.Element: Identifiable, C.Index == Int
where C.Element: Identifiable, C.Index: Hashable

func visit<Root, C: MutableCollection>(context: EntityContext<Root, C>, entities: C)
where C.Element: Aggregate, C.Index == Int
where C.Element: Aggregate, C.Index: Hashable
}
20 changes: 16 additions & 4 deletions Tests/CohesionKitTests/KeyPath/UnsafeMutablePointerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 3,7 @@ import XCTest

class UnsafeMutablePointerTests: XCTestCase {
func test_assign_propertyIsImmutable_propertyIsChanged() {
var hello = Hello()
var hello = Hello(collection: [], singleValue: "hello")

withUnsafeMutablePointer(to: &hello) {
$0.assign("world", to: \Hello.singleValue)
Expand All @@ -13,17 13,29 @@ class UnsafeMutablePointerTests: XCTestCase {
}

func test_assign_keyPathIsCollection_propertyIsImmutable_collectionIsChangedAtIndex() {
var hello = Hello()
var hello = Hello(collection: [1, 2, 3, 4], singleValue: "")

withUnsafeMutablePointer(to: &hello) {
$0.assign(5, to: \Hello.collection, index: 3)
}

XCTAssertEqual(hello.collection, [1, 2, 3, 5])
}

func test_assign_keyPathIsCollection_mutipleAssignments_colllectionIsChanged() {
var hello = Hello(collection: [1, 2, 3, 4], singleValue: "")

withUnsafeMutablePointer(to: &hello) {
$0.assign(4, to: \Hello.collection, index: 0)
$0.assign(3, to: \Hello.collection, index: 0)
$0.assign(2, to: \Hello.collection, index: 0)
}

XCTAssertEqual(hello.collection, [2, 2, 3, 4])
}
}

private struct Hello {
let collection = [1, 2, 3, 4]
let singleValue = "hello"
let collection: [Int]
let singleValue: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 57,14 @@ class IdentityMapStoreVisitorTests: XCTestCase {
}

private class EntityNodeStub<T>: EntityNode<T> {
var observeChildKeyPathIndexCalled: (AnyEntityNode, PartialKeyPath<T>, Int) -> Void = { _, _, _ in }
var observeChildKeyPathIndexCalled: (AnyEntityNode, PartialKeyPath<T>, Any) -> Void = { _, _, _ in }
var observeChildKeyPathOptionalCalled: (AnyEntityNode, PartialKeyPath<T>) -> Void = { _, _ in }

override func observeChild<C: MutableCollection>(
_ childNode: EntityNode<C.Element>,
for keyPath: KeyPath<T, C>,
index: C.Index
) where C.Index == Int {
) {
observeChildKeyPathIndexCalled(childNode, keyPath, index)
}

Expand Down