diff --git a/Sources/CohesionKit/Identity/IdentityStore.swift b/Sources/CohesionKit/Identity/IdentityStore.swift index 38361de..ca82f8b 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) @@ -135,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) -> EntityObserver { identityQueue.sync { - AliasObserver(alias: refAliases[named], registry: registry) + 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<[C.Element]> { + public func find(named: AliasKey) -> EntityObserver { identityQueue.sync { - AliasObserver(alias: refAliases[named], registry: registry) + let node = refAliases[safe: named] + return EntityObserver(alias: node, registry: registry) } } @@ -196,6 +194,20 @@ public class IdentityMap { return node } + private func storeAlias(content: T, key: AliasKey, modifiedAt: Stamp?) { + let aliasNode = refAliases[safe: key] + + 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[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) - // ref might have changed - refAliases.insert(node, key: named) + _ = nodeStore(entity: content, modifiedAt: modifiedAt) + + 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[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[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[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[named] = nil logger?.didUnregisterAlias(named) } /// Removes an alias from the storage public func removeAlias(named: AliasKey) { - refAliases.remove(for: named) + refAliases[named] = nil logger?.didUnregisterAlias(named) } 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/Observer/AliasObserver.swift b/Sources/CohesionKit/Observer/AliasObserver.swift deleted file mode 100644 index 1477f38..0000000 --- a/Sources/CohesionKit/Observer/AliasObserver.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation - -// 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/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/AliasContainer.swift b/Sources/CohesionKit/Storage/AliasContainer.swift new file mode 100644 index 0000000..ace0957 --- /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/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index 5d2273c..2c26aa6 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -1,48 +1,34 @@ /// 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(_ aliasKey: AliasKey) -> EntityNode>? { + get { self[buildKey(for: T.self, key: aliasKey)] as? EntityNode> } + set { self[buildKey(for: T.self, key: aliasKey)] = newValue } + } + subscript(safe key: AliasKey) -> EntityNode> { + mutating get { + self[key: key, default: EntityNode(AliasContainer(key: key), modifiedAt: nil)] } } - subscript(key: AliasKey) -> Observable<[EntityNode]?> { + subscript(key key: AliasKey, default defaultValue: @autoclosure () -> EntityNode>) + -> EntityNode> { mutating get { - if let store = self[AnyHashable(key)] as? Observable<[EntityNode]?> { - return store - } + guard let node = self[key] else { + let node = defaultValue() - let store: Observable<[EntityNode]?> = Observable(value: nil) - self[AnyHashable(key)] = store + self[key] = node - return store + return node + } + return node } } - 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 - } - - mutating func remove(for key: AliasKey) { - (self[AnyHashable(key)] as? Observable<[EntityNode]?>)?.value = nil + private func buildKey(for type: T.Type, key: AliasKey) -> String { + "\(type):\(key.name)" } } 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), 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/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 diff --git a/Tests/CohesionKitTests/Storage/AliasStorageTests.swift b/Tests/CohesionKitTests/Storage/AliasStorageTests.swift index 314e13c..58d2ef5 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_subscriptGetSafe_aliasIsCollection_noValue_emptyAliasContainer() { var storage: AliasStorage = [:] - XCTAssertNotNil(storage[.testCollection]) - XCTAssertNil(storage[.testCollection].value) + XCTAssertNotNil(storage[safe: .testCollection]) + XCTAssertNil(storage[safe: .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[.testCollection] = collectionNode + storage[.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[.test], singleNode) + XCTAssertEqual(storage[.testCollection], collectionNode) } }