From b28007eaa7c513ad5ce74d16385168e9871beb75 Mon Sep 17 00:00:00 2001 From: pjechris Date: Mon, 4 Sep 2023 13:43:47 +0200 Subject: [PATCH 1/6] [alias] added AliasContainer --- .../CohesionKit/Storage/AliasContainer.swift | 39 ++++++++++++ .../Storage/AliasContainerTests.swift | 60 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 Sources/CohesionKit/Storage/AliasContainer.swift create mode 100644 Tests/CohesionKitTests/Storage/AliasContainerTests.swift diff --git a/Sources/CohesionKit/Storage/AliasContainer.swift b/Sources/CohesionKit/Storage/AliasContainer.swift new file mode 100644 index 0000000..79afc21 --- /dev/null +++ b/Sources/CohesionKit/Storage/AliasContainer.swift @@ -0,0 +1,39 @@ + +/// a container to store an aliased object +struct AliasContainer: Identifiable, Aggregate { + var id: String { key.name } + + let key: AliasKey + + var content: T +} + +extension AliasContainer where T: Aggregate { + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath>] { + [.init(\.content)] + } +} + +extension AliasContainer where T: Identifiable { + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath>] { + [.init(\.content)] + } +} + +extension AliasContainer where T: MutableCollection, T.Element: Aggregate, T.Index: Hashable { + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath>] { + [.init(\.content)] + } +} + +extension AliasContainer where T: MutableCollection, T.Element: Identifiable, T.Index: Hashable { + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath>] { + [.init(\.content)] + } +} + +extension AliasContainer { + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath>] { + [] + } +} \ No newline at end of file diff --git a/Tests/CohesionKitTests/Storage/AliasContainerTests.swift b/Tests/CohesionKitTests/Storage/AliasContainerTests.swift new file mode 100644 index 0000000..2933ef5 --- /dev/null +++ b/Tests/CohesionKitTests/Storage/AliasContainerTests.swift @@ -0,0 +1,60 @@ +import XCTest +@testable import CohesionKit + +class AliasContainerTests: XCTestCase { + func test_nestedEntitiesKeyPaths_contentIsAggregate_itContainsContent() { + let container = AliasContainer( + key: .aggregateContainerTest, + content: RootFixture(id: 1, primitive: "", singleNode: .init(id: 1), listNodes: []) + ) + + XCTAssertEqual(container.nestedEntitiesKeyPaths.count, 1) + XCTAssertEqual(container.nestedEntitiesKeyPaths[0].keyPath, \AliasContainer.content) + } + + func test_nestedEntitiesKeyPaths_contentIsIdentifiable_itContainsContent() { + let container = AliasContainer( + key: .identifiableContainerTests, + content: SingleNodeFixture(id: 1) + ) + + XCTAssertEqual(container.nestedEntitiesKeyPaths.count, 1) + XCTAssertEqual(container.nestedEntitiesKeyPaths[0].keyPath, \AliasContainer.content) + } + + func test_nestedEntitiesKeyPaths_contentIsArrayAggregate_itContainsContent() { + let container = AliasContainer( + key: .arrayAggregateContainerTests, + content: [RootFixture(id: 1, primitive: "", singleNode: .init(id: 1), listNodes: [])] + ) + + XCTAssertEqual(container.nestedEntitiesKeyPaths.count, 1) + XCTAssertEqual(container.nestedEntitiesKeyPaths[0].keyPath, \AliasContainer<[RootFixture]>.content) + } + + func test_nestedEntitiesKeyPaths_contentIsArrayIdentifiable_itContainsContent() { + let container = AliasContainer( + key: .arrayIdentifiableContainerTests, + content: [SingleNodeFixture(id: 1)] + ) + + XCTAssertEqual(container.nestedEntitiesKeyPaths.count, 1) + XCTAssertEqual(container.nestedEntitiesKeyPaths[0].keyPath, \AliasContainer<[SingleNodeFixture]>.content) + } +} + +extension AliasKey where T == RootFixture { + fileprivate static let aggregateContainerTest = AliasKey(named: "aggregate") +} + +extension AliasKey where T == SingleNodeFixture { + fileprivate static let identifiableContainerTests = AliasKey(named: "identifiable") +} + +extension AliasKey where T == [SingleNodeFixture] { + fileprivate static let arrayIdentifiableContainerTests = AliasKey(named: "identifiable") +} + +extension AliasKey where T == [RootFixture] { + fileprivate static let arrayAggregateContainerTests = AliasKey(named: "aggregate") +} \ No newline at end of file From bbbd5f62b7cd84b24271155bce1383fd62428c73 Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 26 Sep 2023 17:02:06 +0200 Subject: [PATCH 2/6] [alias] make container value optional --- .../KeyPath/PartialIdentifiableKeyPath.swift | 24 +++++++++++++++++++ .../CohesionKit/Storage/AliasContainer.swift | 2 +- .../Visitor/IdentityMapStoreVisitor.swift | 1 - 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift b/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift index 37379b2..e3a3933 100644 --- a/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift +++ b/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift @@ -58,6 +58,18 @@ public struct PartialIdentifiableKeyPath { } } + public init(_ keyPath: WritableKeyPath) where C.Element: Identifiable, C.Index: Hashable { + self.keyPath = keyPath + self.accept = { parent, root, stamp, visitor in + if let entities = root[keyPath: keyPath] { + visitor.visit( + context: EntityContext(parent: parent, keyPath: keyPath.unwrapped(), stamp: stamp), + entities: entities + ) + } + } + } + public init(_ keyPath: WritableKeyPath) where C.Element: Aggregate, C.Index: Hashable { self.keyPath = keyPath self.accept = { parent, root, stamp, visitor in @@ -68,6 +80,18 @@ public struct PartialIdentifiableKeyPath { } } + public init(_ keyPath: WritableKeyPath) where C.Element: Aggregate, C.Index: Hashable { + self.keyPath = keyPath + self.accept = { parent, root, stamp, visitor in + if let entities = root[keyPath: keyPath] { + visitor.visit( + context: EntityContext(parent: parent, keyPath: keyPath.unwrapped(), stamp: stamp), + entities: entities + ) + } + } + } + public init(wrapper keyPath: WritableKeyPath) { self.keyPath = keyPath self.accept = { parent, root, stamp, visitor in diff --git a/Sources/CohesionKit/Storage/AliasContainer.swift b/Sources/CohesionKit/Storage/AliasContainer.swift index 79afc21..ace0957 100644 --- a/Sources/CohesionKit/Storage/AliasContainer.swift +++ b/Sources/CohesionKit/Storage/AliasContainer.swift @@ -5,7 +5,7 @@ struct AliasContainer: Identifiable, Aggregate { let key: AliasKey - var content: T + var content: T? } extension AliasContainer where T: Aggregate { diff --git a/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift b/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift index 9aa2a51..7e94782 100644 --- a/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift +++ b/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift @@ -38,7 +38,6 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { func visit(context: EntityContext, entities: C) where C.Element: Identifiable, C.Index: Hashable { - for index in entities.indices { context.parent.observeChild( identityMap.nodeStore(entity: entities[index], modifiedAt: context.stamp), From ef3e307f46078ec4a7f01d95779598e314ce4d27 Mon Sep 17 00:00:00 2001 From: pjechris Date: Wed, 27 Sep 2023 18:02:19 +0200 Subject: [PATCH 3/6] [store] store alias using AliasContainer --- .../CohesionKit/Identity/IdentityStore.swift | 86 ++++++++++--------- .../CohesionKit/Storage/AliasStorage.swift | 47 ++-------- 2 files changed, 52 insertions(+), 81 deletions(-) diff --git a/Sources/CohesionKit/Identity/IdentityStore.swift b/Sources/CohesionKit/Identity/IdentityStore.swift index 38361de..6fa5b6c 100644 --- a/Sources/CohesionKit/Identity/IdentityStore.swift +++ b/Sources/CohesionKit/Identity/IdentityStore.swift @@ -48,9 +48,8 @@ public class IdentityMap { let node = nodeStore(entity: entity, modifiedAt: modifiedAt) - if let alias = named { - refAliases.insert(node, key: alias) - logger?.didRegisterAlias(alias) + if let key = named { + storeAlias(content: entity, key: key, modifiedAt: modifiedAt) } return EntityObserver(node: node, registry: registry) @@ -80,9 +79,8 @@ public class IdentityMap { let node = nodeStore(entity: entity, modifiedAt: modifiedAt) - if let alias = named { - refAliases.insert(node, key: alias) - logger?.didRegisterAlias(alias) + if let key = named { + storeAlias(content: entity, key: key, modifiedAt: modifiedAt) } return EntityObserver(node: node, registry: registry) @@ -95,9 +93,8 @@ public class IdentityMap { transaction { let nodes = entities.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } - if let alias = named { - refAliases.insert(nodes, key: alias) - logger?.didRegisterAlias(alias) + if let key = named { + storeAlias(content: entities, key: key, modifiedAt: modifiedAt) } return EntityObserver(nodes: nodes, registry: registry) @@ -110,9 +107,8 @@ public class IdentityMap { transaction { let nodes = entities.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } - if let alias = named { - refAliases.insert(nodes, key: alias) - logger?.didRegisterAlias(alias) + if let key = named { + storeAlias(content: entities, key: key, modifiedAt: modifiedAt) } return EntityObserver(nodes: nodes, registry: registry) @@ -196,6 +192,22 @@ public class IdentityMap { return node } + private func storeAlias(content: T, key: AliasKey, modifiedAt: Stamp?) { + let aliasNode = refAliases[T.self, key: key] ?? EntityNode(AliasContainer(key: key), modifiedAt: nil) + + refAliases[T.self, key: key] = aliasNode + + do { + try aliasNode.updateEntity(AliasContainer(key: key, content: content), modifiedAt: modifiedAt) + + registry.enqueueChange(for: aliasNode) + logger?.didRegisterAlias(key) + } + catch { + + } + } + private func transaction(_ body: () -> T) -> T { identityQueue.sync(flags: .barrier) { let returnValue = body() @@ -260,16 +272,15 @@ extension IdentityMap { @discardableResult public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool { transaction { - guard let entity = refAliases[named].value else { + guard let aliasNode = refAliases[T.self, key: named], var content = aliasNode.ref.value.content else { return false } - var value = entity.ref.value - update(&value) - let node = nodeStore(entity: value, modifiedAt: modifiedAt) + update(&content) + + _ = nodeStore(entity: content, modifiedAt: modifiedAt) - // ref might have changed - refAliases.insert(node, key: named) + storeAlias(content: content, key: named, modifiedAt: modifiedAt) return true } @@ -282,16 +293,15 @@ extension IdentityMap { @discardableResult public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool { transaction { - guard let entity = refAliases[named].value else { + guard let aliasNode = refAliases[T.self, key: named], var content = aliasNode.ref.value.content else { return false } - var value = entity.ref.value - update(&value) - let node = nodeStore(entity: value, modifiedAt: modifiedAt) + update(&content) + + _ = nodeStore(entity: content, modifiedAt: modifiedAt) - // ref might have changed - refAliases.insert(node, key: named) + storeAlias(content: content, key: named, modifiedAt: modifiedAt) return true } @@ -302,20 +312,18 @@ extension IdentityMap { /// the change was applied /// - Returns: true if entity exists and might be updated, false otherwise. The update might **not** be applied if modifiedAt is too old @discardableResult - public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update<[C.Element]>) + public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool where C.Element: Identifiable { transaction { - guard let entities = refAliases[named].value else { + guard let aliasNode = refAliases[C.self, key: named], var content = aliasNode.ref.value.content else { return false } - var values = entities.map(\.ref.value) - update(&values) + update(&content) - let nodes = values.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } + _ = content.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } - // update alias because `update` may have added/removed entities - refAliases.insert(nodes, key: named) + storeAlias(content: content, key: named, modifiedAt: modifiedAt) return true } @@ -326,20 +334,18 @@ extension IdentityMap { /// the change was applied /// - Returns: true if entity exists and might be updated, false otherwise. The update might **not** be applied if modifiedAt is too old @discardableResult - public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update<[C.Element]>) + public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool where C.Element: Aggregate { transaction { - guard let entities = refAliases[named].value else { + guard let aliasNode = refAliases[C.self, key: named], var content = aliasNode.ref.value.content else { return false } - var values = entities.map(\.ref.value) - update(&values) + update(&content) - let nodes = values.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } + _ = content.map { nodeStore(entity: $0, modifiedAt: modifiedAt) } - // update alias because `update` may have added/removed entities - refAliases.insert(nodes, key: named) + storeAlias(content: content, key: named, modifiedAt: modifiedAt) return true } @@ -351,13 +357,13 @@ extension IdentityMap { extension IdentityMap { /// Removes an alias from the storage public func removeAlias(named: AliasKey) { - refAliases.remove(for: named) + refAliases[T.self, key: named] = nil logger?.didUnregisterAlias(named) } /// Removes an alias from the storage public func removeAlias(named: AliasKey) { - refAliases.remove(for: named) + refAliases[C.self, key: named] = nil logger?.didUnregisterAlias(named) } diff --git a/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index 5d2273c..7d1fb5a 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -1,48 +1,13 @@ /// Keep a strong reference on each aliased node -typealias AliasStorage = [AnyHashable: AnyObservable] +typealias AliasStorage = [String: Any] extension AliasStorage { - subscript(key: AliasKey) -> Observable?> { - mutating get { - if let store = self[AnyHashable(key)] as? Observable?> { - return store - } - - let store: Observable?> = Observable(value: nil) - self[AnyHashable(key)] = store - - return store - - } - } - - subscript(key: AliasKey) -> Observable<[EntityNode]?> { - mutating get { - if let store = self[AnyHashable(key)] as? Observable<[EntityNode]?> { - return store - } - - let store: Observable<[EntityNode]?> = Observable(value: nil) - self[AnyHashable(key)] = store - - return store - - } - } - - mutating func insert(_ node: EntityNode, key: AliasKey) { - self[key].value = node - } - - mutating func insert(_ nodes: [EntityNode], key: AliasKey) { - self[key].value = nodes - } - - mutating func remove(for key: AliasKey) { - (self[AnyHashable(key)] as? Observable?>)?.value = nil + subscript(_ type: T.Type, key aliasKey: AliasKey) -> EntityNode>? { + get { self[key(for: T.self, key: aliasKey)] as? EntityNode> } + set { self[key(for: T.self, key: aliasKey)] = newValue } } - mutating func remove(for key: AliasKey) { - (self[AnyHashable(key)] as? Observable<[EntityNode]?>)?.value = nil + private func key(for type: T.Type, alias: AliasKey) -> String { + "\(type):\(alias.name)" } } From b451912975a0357928dd1759ecdf5f86b538da38 Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 28 Sep 2023 13:53:57 +0200 Subject: [PATCH 4/6] [observer] move alias observation inside EntityObserver --- .../CohesionKit/Identity/IdentityStore.swift | 10 +- .../CohesionKit/Observer/AliasObserver.swift | 144 +++++++++--------- .../CohesionKit/Observer/EntityObserver.swift | 14 ++ .../CohesionKit/Storage/AliasStorage.swift | 29 +++- 4 files changed, 118 insertions(+), 79 deletions(-) diff --git a/Sources/CohesionKit/Identity/IdentityStore.swift b/Sources/CohesionKit/Identity/IdentityStore.swift index 6fa5b6c..55fea08 100644 --- a/Sources/CohesionKit/Identity/IdentityStore.swift +++ b/Sources/CohesionKit/Identity/IdentityStore.swift @@ -131,17 +131,19 @@ public class IdentityMap { /// Try to find an entity/aggregate registered under `named` alias /// - Parameter named: the alias to look for - public func find(named: AliasKey) -> AliasObserver { + public func find(named: AliasKey) -> AliasObserver { identityQueue.sync { - AliasObserver(alias: refAliases[named], registry: registry) + let node = refAliases[key: named] + return EntityObserver(alias: node, registry: registry) } } /// Try to find a collected registered under `named` alias /// - Returns: an observer returning the alias value. Note that the value will be an Array - public func find(named: AliasKey) -> AliasObserver<[C.Element]> { + public func find(named: AliasKey) -> AliasObserver { identityQueue.sync { - AliasObserver(alias: refAliases[named], registry: registry) + let node = refAliases[key: named] + return EntityObserver(alias: node, registry: registry) } } diff --git a/Sources/CohesionKit/Observer/AliasObserver.swift b/Sources/CohesionKit/Observer/AliasObserver.swift index 1477f38..105fc20 100644 --- a/Sources/CohesionKit/Observer/AliasObserver.swift +++ b/Sources/CohesionKit/Observer/AliasObserver.swift @@ -1,85 +1,87 @@ import Foundation +public typealias AliasObserver = EntityObserver + // A type registering observers over an aliased entity -public struct AliasObserver: Observer { - typealias OnChangeClosure = (T?) -> Void +// public struct AliasObserver: Observer { +// typealias OnChangeClosure = (T?) -> Void - public var value: T? - /// a closure redirecting to the right observe method depending on T type - let createObserve: (@escaping OnChangeClosure) -> Subscription +// public var value: T? +// /// a closure redirecting to the right observe method depending on T type +// let createObserve: (@escaping OnChangeClosure) -> Subscription - /// create an observer for a single entity node ref - init(alias: Observable?>, registry: ObserverRegistry) { - self.value = alias.value?.ref.value - self.createObserve = { - Self.createObserve(for: alias, registry: registry, onChange: $0) - } - } +// /// create an observer for a single entity node ref +// init(alias: Observable?>, registry: ObserverRegistry) { +// self.value = alias.value?.ref.value +// self.createObserve = { +// Self.createObserve(for: alias, registry: registry, onChange: $0) +// } +// } - /// create an observer for a list of node ref - init(alias: Observable<[EntityNode]?>, registry: ObserverRegistry) where T == Array { - self.value = alias.value?.map(\.ref.value) - self.createObserve = { - Self.createObserve(for: alias, registry: registry, onChange: $0) - } - } +// /// create an observer for a list of node ref +// init(alias: Observable<[EntityNode]?>, registry: ObserverRegistry) where T == Array { +// self.value = alias.value?.map(\.ref.value) +// self.createObserve = { +// Self.createObserve(for: alias, registry: registry, onChange: $0) +// } +// } - public func observe(onChange: @escaping (T?) -> Void) -> Subscription { - createObserve(onChange) - } -} +// public func observe(onChange: @escaping (T?) -> Void) -> Subscription { +// createObserve(onChange) +// } +// } -extension AliasObserver { - /// Create an observer sending updates every time: - /// - the ref node change - /// - the ref node value change - private static func createObserve( - for alias: Observable?>, - registry: ObserverRegistry, - onChange: @escaping OnChangeClosure - ) -> Subscription { - // register for current alias value - var entityChangesSubscription: Subscription? = alias - .value - .map { node in registry.addObserver(node: node, initial: true, onChange: onChange) } +// extension AliasObserver { +// /// Create an observer sending updates every time: +// /// - the ref node change +// /// - the ref node value change +// private static func createObserve( +// for alias: Observable?>, +// registry: ObserverRegistry, +// onChange: @escaping OnChangeClosure +// ) -> Subscription { +// // register for current alias value +// var entityChangesSubscription: Subscription? = alias +// .value +// .map { node in registry.addObserver(node: node, initial: true, onChange: onChange) } - // subscribe to alias changes - let subscription = alias.addObserver { node in - // update entity changes subscription - entityChangesSubscription = node.map { registry.addObserver(node: $0, initial: true, onChange: onChange) } - } +// // subscribe to alias changes +// let subscription = alias.addObserver { node in +// // update entity changes subscription +// entityChangesSubscription = node.map { registry.addObserver(node: $0, initial: true, onChange: onChange) } +// } - return Subscription { - subscription.unsubscribe() - entityChangesSubscription?.unsubscribe() - } - } +// return Subscription { +// subscription.unsubscribe() +// entityChangesSubscription?.unsubscribe() +// } +// } - /// Create an observer sending updates every time: - /// - the ref node change - /// - any of the ref node element change - private static func createObserve( - for alias: Observable<[EntityNode]?>, - registry: ObserverRegistry, - onChange: @escaping OnChangeClosure - ) -> Subscription where T == Array { - // register for current alias value - var entitiesChangesSubscriptions: Subscription? = alias - .value - .map { nodes in EntityObserver(nodes: nodes, registry: registry) }? - .observe(onChange: onChange) +// /// Create an observer sending updates every time: +// /// - the ref node change +// /// - any of the ref node element change +// private static func createObserve( +// for alias: Observable<[EntityNode]?>, +// registry: ObserverRegistry, +// onChange: @escaping OnChangeClosure +// ) -> Subscription where T == Array { +// // register for current alias value +// var entitiesChangesSubscriptions: Subscription? = alias +// .value +// .map { nodes in EntityObserver(nodes: nodes, registry: registry) }? +// .observe(onChange: onChange) - // Subscribe to alias ref changes and to any changes made on the ref collection nodes. - let subscription = alias.addObserver { nodes in - let nodeObservers = nodes.map { EntityObserver(nodes: $0, registry: registry) } +// // Subscribe to alias ref changes and to any changes made on the ref collection nodes. +// let subscription = alias.addObserver { nodes in +// let nodeObservers = nodes.map { EntityObserver(nodes: $0, registry: registry) } - // update collection changes subscription - entitiesChangesSubscriptions = nodeObservers?.observe(onChange: onChange) - } +// // update collection changes subscription +// entitiesChangesSubscriptions = nodeObservers?.observe(onChange: onChange) +// } - return Subscription { - subscription.unsubscribe() - entitiesChangesSubscriptions?.unsubscribe() - } - } -} +// return Subscription { +// subscription.unsubscribe() +// entitiesChangesSubscriptions?.unsubscribe() +// } +// } +// } diff --git a/Sources/CohesionKit/Observer/EntityObserver.swift b/Sources/CohesionKit/Observer/EntityObserver.swift index be1668a..96036c6 100644 --- a/Sources/CohesionKit/Observer/EntityObserver.swift +++ b/Sources/CohesionKit/Observer/EntityObserver.swift @@ -22,6 +22,20 @@ public struct EntityObserver: Observer { } } + init(alias node: EntityNode>, registry: ObserverRegistry) + where T == Optional { + self.init(value: node.ref.value.content) { onChange in + registry.addObserver(node: node, initial: true, onChange: { container in + onChange(container.content) + }) + } + } + + init(value: T, createObserver: @escaping (@escaping OnChange) -> Subscription) { + self.value = value + self.createObserver = createObserver + } + public func observe(onChange: @escaping OnChange) -> Subscription { createObserver(onChange) } diff --git a/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index 7d1fb5a..e5759d4 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -3,11 +3,32 @@ typealias AliasStorage = [String: Any] extension AliasStorage { subscript(_ type: T.Type, key aliasKey: AliasKey) -> EntityNode>? { - get { self[key(for: T.self, key: aliasKey)] as? EntityNode> } - set { self[key(for: T.self, key: aliasKey)] = newValue } + get { self[buildKey(for: T.self, key: aliasKey)] as? EntityNode> } + set { self[buildKey(for: T.self, key: aliasKey)] = newValue } } - private func key(for type: T.Type, alias: AliasKey) -> String { - "\(type):\(alias.name)" + subscript(key key: AliasKey) -> EntityNode> { + mutating get { + self[key: key, default: EntityNode(AliasContainer(key: key), modifiedAt: nil)] + } + } + + subscript(key key: AliasKey, default defaultValue: @autoclosure () -> EntityNode>) + -> EntityNode> { + mutating get { + guard let node = self[T.self, key: key] else { + let node = defaultValue() + + self[T.self, key: key] = node + + return node + } + + return node + } + } + + private func buildKey(for type: T.Type, key: AliasKey) -> String { + "\(type):\(key.name)" } } From fcd67357130ff9934bb8293aae75dc89bc5594bf Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 28 Sep 2023 14:44:58 +0200 Subject: [PATCH 5/6] [tests] revamped tests for aliases --- Tests/CohesionKitTests/IdentityMapTests.swift | 37 +++++ .../Observer/AliasObserverTests.swift | 151 ------------------ .../Observer/Stub/ObserverRegistryStub.swift | 11 +- .../Storage/AliasStorageTests.swift | 35 ++-- 4 files changed, 56 insertions(+), 178 deletions(-) delete mode 100644 Tests/CohesionKitTests/Observer/AliasObserverTests.swift diff --git a/Tests/CohesionKitTests/IdentityMapTests.swift b/Tests/CohesionKitTests/IdentityMapTests.swift index 33035ca..7c4afec 100644 --- a/Tests/CohesionKitTests/IdentityMapTests.swift +++ b/Tests/CohesionKitTests/IdentityMapTests.swift @@ -140,6 +140,17 @@ class IdentityMapTests: XCTestCase { wait(for: [expectation], timeout: 0) } + + func test_storeAlias_itEnqueuesAliasInRegistry() { + let root = SingleNodeFixture(id: 1) + let registry = ObserverRegistryStub() + let identityMap = IdentityMap(registry: registry) + + withExtendedLifetime(identityMap.store(entity: root, named: .test)) { + XCTAssertTrue(registry.hasPendingChange(for: AliasContainer.self)) + XCTAssertTrue(registry.hasPendingChange(for: SingleNodeFixture.self)) + } + } } // MARK: Find @@ -317,6 +328,28 @@ extension IdentityMapTests { wait(for: [expectation], timeout: 0.5) } } + + func test_updateNamed_entityIsCollection_itEnqueuesNestedObjectsInRegistry() { + let registry = ObserverRegistryStub() + let identityMap = IdentityMap(registry: registry) + let initialValue = RootFixture( + id: 1, + primitive: "", + singleNode: .init(id: 1), + listNodes: [] + ) + let singleNodeUpdate = SingleNodeFixture(id: 1, primitive: "update") + + _ = identityMap.store(entity: initialValue, named: .root) + + registry.clearPendingChangesStub() + + identityMap.update(named: .root) { + $0.singleNode = singleNodeUpdate + } + + XCTAssertTrue(registry.hasPendingChange(for: singleNodeUpdate)) + } } private extension AliasKey where T == SingleNodeFixture { @@ -326,3 +359,7 @@ private extension AliasKey where T == SingleNodeFixture { private extension AliasKey where T == [SingleNodeFixture] { static let listOfNodes = AliasKey(named: "listOfNodes") } + +private extension AliasKey where T == RootFixture { + static let root = AliasKey(named: "root") +} diff --git a/Tests/CohesionKitTests/Observer/AliasObserverTests.swift b/Tests/CohesionKitTests/Observer/AliasObserverTests.swift deleted file mode 100644 index 3dc3cdb..0000000 --- a/Tests/CohesionKitTests/Observer/AliasObserverTests.swift +++ /dev/null @@ -1,151 +0,0 @@ -import XCTest -@testable import CohesionKit - -class AliasObserverTests: XCTestCase { - func test_observe_refValueChanged_onChangeIsCalled() { - let ref = Observable(value: Optional.some(EntityNode(SingleNodeFixture(id: 1), modifiedAt: 0))) - let registry = ObserverRegistry(queue: .main) - let observer = AliasObserver(alias: ref, registry: registry) - let newValue = SingleNodeFixture(id: 2) - let expectation = XCTestExpectation() - var droppedFirst = false - - let subscription = observer.observe { - guard droppedFirst else { - droppedFirst = true - return - } - - XCTAssertEqual($0, newValue) - expectation.fulfill() - } - - withExtendedLifetime(subscription) { - let newNode = EntityNode(newValue, modifiedAt: 0) - - ref.value = newNode - - registry.enqueueChange(for: newNode) - registry.postChanges() - } - - wait(for: [expectation], timeout: 1) - } - - func test_observe_registryPostEntityNotification_onChangeIsCalled() throws { - let node = EntityNode(RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 0), listNodes: []), modifiedAt: 0) - let registry = ObserverRegistry(queue: .main) - let observer = AliasObserver(alias: Observable(value: node), registry: registry) - let newValue = RootFixture(id: 1, primitive: "new value", singleNode: SingleNodeFixture(id: 1), listNodes: []) - let expectation = XCTestExpectation() - var droppedFirst = false - - let subscription = observer.observe { - guard droppedFirst else { - droppedFirst = true - return - } - - XCTAssertEqual($0, newValue) - expectation.fulfill() - } - - try withExtendedLifetime(subscription) { - try node.updateEntity(newValue, modifiedAt: nil) - - registry.enqueueChange(for: node) - registry.postChanges() - - wait(for: [expectation], timeout: 1) - } - } - - func test_observe_refValueChanged_entityIsUpdated_onChangeIsCalled() throws { - let ref = Observable(value: Optional.some(EntityNode(SingleNodeFixture(id: 1), modifiedAt: 0))) - let registry = ObserverRegistry(queue: .main) - let observer = AliasObserver(alias: ref, registry: registry) - let newNode = EntityNode(SingleNodeFixture(id: 2), modifiedAt: 0) - let newValue = SingleNodeFixture(id: 3) - var lastReceivedValue: SingleNodeFixture? - let expectation = XCTestExpectation() - - expectation.expectedFulfillmentCount = 3 - - let subscription = observer.observe { - lastReceivedValue = $0 - expectation.fulfill() - } - - try withExtendedLifetime(subscription) { - ref.value = newNode - - try newNode.updateEntity(newValue, modifiedAt: nil) - - registry.enqueueChange(for: newNode) - registry.postChanges() - } - - wait(for: [expectation], timeout: 1) - XCTAssertEqual(lastReceivedValue, newValue) - } - - func test_observe_subscriptionIsCancelled_unsubscribeToUpdates() throws { - let initialValue = SingleNodeFixture(id: 1) - let registry = ObserverRegistry(queue: .main) - let node = EntityNode(initialValue, modifiedAt: 0) - let ref = Observable(value: Optional.some(node)) - let observer = AliasObserver(alias: ref, registry: registry) - let newValue = SingleNodeFixture(id: 3) - var lastReceivedValue: SingleNodeFixture? - var firstDropped = false - - _ = observer.observe { - guard firstDropped else { - firstDropped = true - return - } - - lastReceivedValue = $0 - } - - try node.updateEntity(newValue, modifiedAt: 1) - - registry.enqueueChange(for: node) - registry.postChanges() - - XCTAssertNil(lastReceivedValue) - } - - func test_observeArray_registryPostNotificationForElement_onChangeIsCalled() throws { - let expectation = XCTestExpectation() - let nodes = [ - EntityNode(SingleNodeFixture(id: 1), modifiedAt: 0), - EntityNode(SingleNodeFixture(id: 2), modifiedAt: 0) - ] - let ref = Observable(value: Optional.some(nodes)) - let registry = ObserverRegistry(queue: .main) - let observer = AliasObserver(alias: ref, registry: registry) - let update = SingleNodeFixture(id: 1, primitive: "Update") - var firstDropped = false - var subscription: Subscription? - - subscription = observer.observe { value in - guard firstDropped else { - firstDropped = true - return - } - - withExtendedLifetime(subscription) { - expectation.fulfill() - XCTAssertEqual(value?.first, update) - } - } - - try nodes[0].updateEntity(update, modifiedAt: nil) - - registry.enqueueChange(for: nodes[0]) - registry.postChanges() - - wait(for: [expectation], timeout: 1) - } -} diff --git a/Tests/CohesionKitTests/Observer/Stub/ObserverRegistryStub.swift b/Tests/CohesionKitTests/Observer/Stub/ObserverRegistryStub.swift index dcaa18f..746c40e 100644 --- a/Tests/CohesionKitTests/Observer/Stub/ObserverRegistryStub.swift +++ b/Tests/CohesionKitTests/Observer/Stub/ObserverRegistryStub.swift @@ -6,7 +6,6 @@ class ObserverRegistryStub: ObserverRegistry { /// Enqueued changes. Not typed. A same change could be enqueue multiple times (for testing purposes!) private var pendingChangesStub: [Any] = [] - override func enqueueChange(for node: EntityNode) { pendingChangesStub.append(node) enqueueChangeCalled(AnyHashable(node)) @@ -17,8 +16,16 @@ class ObserverRegistryStub: ObserverRegistry { pendingChangesStub.contains { ($0 as? EntityNode)?.ref.value == entity } } + func hasPendingChange(for _: T.Type) -> Bool { + pendingChangesStub.contains { ($0 as? EntityNode) != nil } + } + /// number of times change has been inserted for this entity func pendingChangeCount(for entity: T) -> Int { pendingChangesStub.filter { ($0 as? EntityNode)?.ref.value == entity }.count } -} \ No newline at end of file + + func clearPendingChangesStub() { + pendingChangesStub.removeAll() + } +} diff --git a/Tests/CohesionKitTests/Storage/AliasStorageTests.swift b/Tests/CohesionKitTests/Storage/AliasStorageTests.swift index 314e13c..8813030 100644 --- a/Tests/CohesionKitTests/Storage/AliasStorageTests.swift +++ b/Tests/CohesionKitTests/Storage/AliasStorageTests.swift @@ -2,38 +2,23 @@ import XCTest @testable import CohesionKit class AliasStorageTests: XCTestCase { - func test_subscriptGet_aliasIsCollection_noValue_returnObservable() { + func test_subscriptGet_aliasIsCollection_noValue_emptyAliasContainer() { var storage: AliasStorage = [:] - XCTAssertNotNil(storage[.testCollection]) - XCTAssertNil(storage[.testCollection].value) + XCTAssertNotNil(storage[key: .testCollection]) + XCTAssertNil(storage[key: .testCollection].ref.value.content) } - func test_subscriptGet_twoAliasWithSameNameButDifferentType_returnBothCollections() { + func test_subscriptGet_aliasHasSameNameThanAnotherType_itReturnsAliasContainer() { var storage: AliasStorage = [:] + let singleNode = EntityNode(AliasContainer(key: .test, content: 1), modifiedAt: 0) + let collectionNode = EntityNode(AliasContainer(key: .testCollection, content: [2, 3]), modifiedAt: 0) - storage[.testCollection].value = [EntityNode(1, modifiedAt: 0), EntityNode(2, modifiedAt: 0)] - storage[.test].value = EntityNode(3, modifiedAt: 0) + storage[[Int].self, key: .testCollection] = collectionNode + storage[Int.self, key: .test] = singleNode - XCTAssertEqual(storage.count, 2) - } - - func test_subscriptGet_valueSet_returnValue() { - var storage: AliasStorage = [:] - let expectedValue = [EntityNode(1, modifiedAt: 0)] - - storage[.testCollection].value = expectedValue - - XCTAssertEqual(storage[.testCollection].value, expectedValue) - } - - func test_remove_aliasInsertedBefore_itRemovesValue() { - var storage: AliasStorage = [:] - - storage.insert(EntityNode(ref: Observable(value: 1), modifiedAt: nil), key: .test) - storage.remove(for: .test) - - XCTAssertNil(storage[.test].value) + XCTAssertEqual(storage[key: .test], singleNode) + XCTAssertEqual(storage[key: .testCollection], collectionNode) } } From f04089ce40f5d95514f9f69c599ae674f75c0133 Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 28 Sep 2023 15:57:04 +0200 Subject: [PATCH 6/6] [alias] simplified subscript --- .../CohesionKit/Identity/IdentityStore.swift | 24 +++-- .../CohesionKit/Observer/AliasObserver.swift | 87 ------------------- .../CohesionKit/Storage/AliasStorage.swift | 8 +- .../Storage/AliasStorageTests.swift | 14 +-- 4 files changed, 22 insertions(+), 111 deletions(-) delete mode 100644 Sources/CohesionKit/Observer/AliasObserver.swift diff --git a/Sources/CohesionKit/Identity/IdentityStore.swift b/Sources/CohesionKit/Identity/IdentityStore.swift index 55fea08..ca82f8b 100644 --- a/Sources/CohesionKit/Identity/IdentityStore.swift +++ b/Sources/CohesionKit/Identity/IdentityStore.swift @@ -131,18 +131,18 @@ public class IdentityMap { /// Try to find an entity/aggregate registered under `named` alias /// - Parameter named: the alias to look for - public func find(named: AliasKey) -> AliasObserver { + public func find(named: AliasKey) -> EntityObserver { identityQueue.sync { - let node = refAliases[key: named] + let node = refAliases[safe: named] return EntityObserver(alias: node, registry: registry) } } /// Try to find a collected registered under `named` alias /// - Returns: an observer returning the alias value. Note that the value will be an Array - public func find(named: AliasKey) -> AliasObserver { + public func find(named: AliasKey) -> EntityObserver { identityQueue.sync { - let node = refAliases[key: named] + let node = refAliases[safe: named] return EntityObserver(alias: node, registry: registry) } } @@ -195,9 +195,7 @@ public class IdentityMap { } private func storeAlias(content: T, key: AliasKey, modifiedAt: Stamp?) { - let aliasNode = refAliases[T.self, key: key] ?? EntityNode(AliasContainer(key: key), modifiedAt: nil) - - refAliases[T.self, key: key] = aliasNode + let aliasNode = refAliases[safe: key] do { try aliasNode.updateEntity(AliasContainer(key: key, content: content), modifiedAt: modifiedAt) @@ -274,7 +272,7 @@ extension IdentityMap { @discardableResult public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool { transaction { - guard let aliasNode = refAliases[T.self, key: named], var content = aliasNode.ref.value.content else { + guard let aliasNode = refAliases[named], var content = aliasNode.ref.value.content else { return false } @@ -295,7 +293,7 @@ extension IdentityMap { @discardableResult public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool { transaction { - guard let aliasNode = refAliases[T.self, key: named], var content = aliasNode.ref.value.content else { + guard let aliasNode = refAliases[named], var content = aliasNode.ref.value.content else { return false } @@ -317,7 +315,7 @@ extension IdentityMap { public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool where C.Element: Identifiable { transaction { - guard let aliasNode = refAliases[C.self, key: named], var content = aliasNode.ref.value.content else { + guard let aliasNode = refAliases[named], var content = aliasNode.ref.value.content else { return false } @@ -339,7 +337,7 @@ extension IdentityMap { public func update(named: AliasKey, modifiedAt: Stamp? = nil, update: Update) -> Bool where C.Element: Aggregate { transaction { - guard let aliasNode = refAliases[C.self, key: named], var content = aliasNode.ref.value.content else { + guard let aliasNode = refAliases[named], var content = aliasNode.ref.value.content else { return false } @@ -359,13 +357,13 @@ extension IdentityMap { extension IdentityMap { /// Removes an alias from the storage public func removeAlias(named: AliasKey) { - refAliases[T.self, key: named] = nil + refAliases[named] = nil logger?.didUnregisterAlias(named) } /// Removes an alias from the storage public func removeAlias(named: AliasKey) { - refAliases[C.self, key: named] = nil + refAliases[named] = nil logger?.didUnregisterAlias(named) } diff --git a/Sources/CohesionKit/Observer/AliasObserver.swift b/Sources/CohesionKit/Observer/AliasObserver.swift deleted file mode 100644 index 105fc20..0000000 --- a/Sources/CohesionKit/Observer/AliasObserver.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation - -public typealias AliasObserver = EntityObserver - -// A type registering observers over an aliased entity -// public struct AliasObserver: Observer { -// typealias OnChangeClosure = (T?) -> Void - -// public var value: T? -// /// a closure redirecting to the right observe method depending on T type -// let createObserve: (@escaping OnChangeClosure) -> Subscription - -// /// create an observer for a single entity node ref -// init(alias: Observable?>, registry: ObserverRegistry) { -// self.value = alias.value?.ref.value -// self.createObserve = { -// Self.createObserve(for: alias, registry: registry, onChange: $0) -// } -// } - -// /// create an observer for a list of node ref -// init(alias: Observable<[EntityNode]?>, registry: ObserverRegistry) where T == Array { -// self.value = alias.value?.map(\.ref.value) -// self.createObserve = { -// Self.createObserve(for: alias, registry: registry, onChange: $0) -// } -// } - -// public func observe(onChange: @escaping (T?) -> Void) -> Subscription { -// createObserve(onChange) -// } -// } - -// extension AliasObserver { -// /// Create an observer sending updates every time: -// /// - the ref node change -// /// - the ref node value change -// private static func createObserve( -// for alias: Observable?>, -// registry: ObserverRegistry, -// onChange: @escaping OnChangeClosure -// ) -> Subscription { -// // register for current alias value -// var entityChangesSubscription: Subscription? = alias -// .value -// .map { node in registry.addObserver(node: node, initial: true, onChange: onChange) } - -// // subscribe to alias changes -// let subscription = alias.addObserver { node in -// // update entity changes subscription -// entityChangesSubscription = node.map { registry.addObserver(node: $0, initial: true, onChange: onChange) } -// } - -// return Subscription { -// subscription.unsubscribe() -// entityChangesSubscription?.unsubscribe() -// } -// } - -// /// Create an observer sending updates every time: -// /// - the ref node change -// /// - any of the ref node element change -// private static func createObserve( -// for alias: Observable<[EntityNode]?>, -// registry: ObserverRegistry, -// onChange: @escaping OnChangeClosure -// ) -> Subscription where T == Array { -// // register for current alias value -// var entitiesChangesSubscriptions: Subscription? = alias -// .value -// .map { nodes in EntityObserver(nodes: nodes, registry: registry) }? -// .observe(onChange: onChange) - -// // Subscribe to alias ref changes and to any changes made on the ref collection nodes. -// let subscription = alias.addObserver { nodes in -// let nodeObservers = nodes.map { EntityObserver(nodes: $0, registry: registry) } - -// // update collection changes subscription -// entitiesChangesSubscriptions = nodeObservers?.observe(onChange: onChange) -// } - -// return Subscription { -// subscription.unsubscribe() -// entitiesChangesSubscriptions?.unsubscribe() -// } -// } -// } diff --git a/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index e5759d4..2c26aa6 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -2,12 +2,12 @@ typealias AliasStorage = [String: Any] extension AliasStorage { - subscript(_ type: T.Type, key aliasKey: AliasKey) -> EntityNode>? { + subscript(_ aliasKey: AliasKey) -> EntityNode>? { get { self[buildKey(for: T.self, key: aliasKey)] as? EntityNode> } set { self[buildKey(for: T.self, key: aliasKey)] = newValue } } - subscript(key key: AliasKey) -> EntityNode> { + subscript(safe key: AliasKey) -> EntityNode> { mutating get { self[key: key, default: EntityNode(AliasContainer(key: key), modifiedAt: nil)] } @@ -16,10 +16,10 @@ extension AliasStorage { subscript(key key: AliasKey, default defaultValue: @autoclosure () -> EntityNode>) -> EntityNode> { mutating get { - guard let node = self[T.self, key: key] else { + guard let node = self[key] else { let node = defaultValue() - self[T.self, key: key] = node + self[key] = node return node } diff --git a/Tests/CohesionKitTests/Storage/AliasStorageTests.swift b/Tests/CohesionKitTests/Storage/AliasStorageTests.swift index 8813030..58d2ef5 100644 --- a/Tests/CohesionKitTests/Storage/AliasStorageTests.swift +++ b/Tests/CohesionKitTests/Storage/AliasStorageTests.swift @@ -2,11 +2,11 @@ import XCTest @testable import CohesionKit class AliasStorageTests: XCTestCase { - func test_subscriptGet_aliasIsCollection_noValue_emptyAliasContainer() { + func test_subscriptGetSafe_aliasIsCollection_noValue_emptyAliasContainer() { var storage: AliasStorage = [:] - XCTAssertNotNil(storage[key: .testCollection]) - XCTAssertNil(storage[key: .testCollection].ref.value.content) + XCTAssertNotNil(storage[safe: .testCollection]) + XCTAssertNil(storage[safe: .testCollection].ref.value.content) } func test_subscriptGet_aliasHasSameNameThanAnotherType_itReturnsAliasContainer() { @@ -14,11 +14,11 @@ class AliasStorageTests: XCTestCase { let singleNode = EntityNode(AliasContainer(key: .test, content: 1), modifiedAt: 0) let collectionNode = EntityNode(AliasContainer(key: .testCollection, content: [2, 3]), modifiedAt: 0) - storage[[Int].self, key: .testCollection] = collectionNode - storage[Int.self, key: .test] = singleNode + storage[.testCollection] = collectionNode + storage[.test] = singleNode - XCTAssertEqual(storage[key: .test], singleNode) - XCTAssertEqual(storage[key: .testCollection], collectionNode) + XCTAssertEqual(storage[.test], singleNode) + XCTAssertEqual(storage[.testCollection], collectionNode) } }