From 2af3b19b9dfce6ea246e7f5f84347da3e00fe5fe Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 21 Dec 2023 16:33:56 +0100 Subject: [PATCH 1/3] [store] IdentityMap -> EntityStore --- Example/Example/Data/MatchRepository.swift | 20 +- README.md | 66 +++--- .../Combine/Publisher+CohesionKit.swift | 24 +-- Sources/CohesionKit/EntityStore.swift | 13 +- Sources/CohesionKit/Logger.swift | 8 +- Sources/CohesionKit/Stamp/Date+Stamp.swift | 2 +- .../CohesionKit/Visitor/EntityContext.swift | 2 +- ...Visitor.swift => EntityStoreVisitor.swift} | 18 +- Tests/CohesionKitTests/EntityStoreTests.swift | 196 +++++++++--------- ...ts.swift => EntityStoreVisitorTests.swift} | 8 +- 10 files changed, 180 insertions(+), 177 deletions(-) rename Sources/CohesionKit/Visitor/{IdentityMapStoreVisitor.swift => EntityStoreVisitor.swift} (75%) rename Tests/CohesionKitTests/Visitor/{IdentityMapStoreVisitorTests.swift => EntityStoreVisitorTests.swift} (91%) diff --git a/Example/Example/Data/MatchRepository.swift b/Example/Example/Data/MatchRepository.swift index f89b7cb..10a8ead 100644 --- a/Example/Example/Data/MatchRepository.swift +++ b/Example/Example/Data/MatchRepository.swift @@ -3,38 +3,38 @@ import Combine import CohesionKit class MatchRepository { - private static let identityMap = IdentityMap() - private lazy var identityMap = Self.identityMap + private static let entityStore = EntityStore() + private lazy var entityStore = Self.entityStore /// load matches with their markets and outcomes from Data.swift func loadMatches() -> AnyPublisher<[MatchMarkets], Never> { let matches = MatchMarkets.simulatedMatches - /// store the match and its markets into the identityMap - return identityMap.store(entities: matches, modifiedAt: MatchMarkets.simulatedFetchedDate.stamp).asPublisher + /// store the match and its markets into the entityStore + return entityStore.store(entities: matches, modifiedAt: MatchMarkets.simulatedFetchedDate.stamp).asPublisher } - + /// observe primary (first) match market changes (for this sample changes are generated randomly) /// - Returns: the match with all its markets including updates for primary market func observePrimaryMarket(for match: Match) -> AnyPublisher { let matchMarkets = MatchMarkets.simulatedMatches.first { $0.match.id == match.id }! var cancellables: Set = [] - + for outcome in matchMarkets.primaryMarket.outcomes { generateRandomChanges(for: outcome) - .sink(receiveValue: { [identityMap] in - _ = identityMap.store(entity: $0) + .sink(receiveValue: { [entityStore] in + _ = entityStore.store(entity: $0) }) .store(in: &cancellables) } /// for the test we consider that `loadMatches` was already called and thus markets already stored - return identityMap.find(MatchMarkets.self, id: match.id)! + return entityStore.find(MatchMarkets.self, id: match.id)! .asPublisher .handleEvents(receiveCancel: { cancellables.removeAll() }) .eraseToAnyPublisher() } - + private func generateRandomChanges(for outcome: Outcome) -> AnyPublisher { Timer .publish(every: 3, on: .main, in: .common) diff --git a/README.md b/README.md index fd788b4..3ce7cce 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,13 @@ Library comes with an [example project](https://github.com/pjechris/CohesionKit/ ### Storing an object -First create an instance of `IdentityMap`: +First create an instance of `EntityStore`: ```swift -let identityMap = IdentityMap() +let entityStore = EntityStore() ``` -`IdentityMap` let you store `Identifiable` objects: +`EntityStore` let you store `Identifiable` objects: ```swift struct Book: Identifiable { @@ -89,39 +89,39 @@ struct Book: Identifiable { let book = Book(id: "ABCD", name: "My Book") -identityMap.store(book) +entityStore.store(book) ``` Then You can retrieve the object from anywhere in your code: ```swift // somewhere else in the code -identityMap.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book") +entityStore.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book") ``` ### Observing changes -Every time data is updated in `IdentityMap` triggers a notification to any registered observer. To register yourself as an observer just use result from `store` or `find` methods: +Every time data is updated in `EntityStore` triggers a notification to any registered observer. To register yourself as an observer just use result from `store` or `find` methods: ```swift func findBooks() -> some Publisher<[Book], Error> { // 1. load data using URLSession URLSession(...) - // 2. store data inside our identityMap - .store(in: identityMap) + // 2. store data inside our entityStore + .store(in: entityStore) .sink { ... } .store(in: &cancellables) } ``` ```swift -identityMap.find(Book.self, id: 1)? +entityStore.find(Book.self, id: 1)? .asPublisher .sink { ... } .store(in: &cancellables) ``` -> CohesionKit has a [weak memory policy](#weak-memory-management) you should read about. As such, returned value from identityMap.store must be strongly retained to not lose value. +> CohesionKit has a [weak memory policy](#weak-memory-management) you should read about. As such, returned value from entityStore.store must be strongly retained to not lose value. > For brievety, next examples will omit `.sink { ... }.store(in:&cancellables)`. @@ -160,11 +160,11 @@ let authorBooks = AuthorBooks( ] ) -identityMap.store(authorBooks) +entityStore.store(authorBooks) -identityMap.find(Author.self, id: 1) // George R.R Martin -identityMap.find(Book.self, id: "ACK") // A Clash of Kings -identityMap.find(Book.self, id: "ADD") // A Dance with Dragons +entityStore.find(Author.self, id: 1) // George R.R Martin +entityStore.find(Book.self, id: "ACK") // A Clash of Kings +entityStore.find(Book.self, id: "ADD") // A Dance with Dragons ``` You can also modify any of them however you want. Notice the change is visible from the object itself AND from aggregate objects: @@ -172,10 +172,10 @@ You can also modify any of them however you want. Notice the change is visible f ```swift let newAuthor = Author(id: 1, name: "George R.R MartinI") -identityMap.store(newAuthor) +entityStore.store(newAuthor) -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] +entityStore.find(Author.self, id: 1) // George R.R MartinI +entityStore.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` needs to implement `Aggregate` and declare `books` as nested entity. @@ -184,7 +184,7 @@ identityMap.find(AuthorBooks.self, id: 1) // George R.R MartinI + [A Clash of Ki ### Storing vs Updating -For now we only focused on `identityMap.store` but CohesionKit comes with another method to store data: `identityMap.update`. +For now we only focused on `entityStore.store` but CohesionKit comes with another method to store data: `entityStore.update`. Sometimes both can be used but they each have a different purpose: @@ -244,21 +244,21 @@ extension AliasKey where T == User { static let currentUser = AliasKey("user") } -identityMap.store(currentUser, named: .currentUser) +entityStore.store(currentUser, named: .currentUser) ``` Then request it somewhere else: ```swift -identityMap.find(named: .currentUser) // return the current user +entityStore.find(named: .currentUser) // return the current user ``` Compared to regular entities, aliased objects are long-live objects: they will be kept in the storage **even if no one observes them**. This allow registered observers to be notified when alias value change: ```swift -identityMap.removeAlias(named: .currentUser) // observers will be notified currentUser is nil. +entityStore.removeAlias(named: .currentUser) // observers will be notified currentUser is nil. -identityMap.store(newCurrentUser, named: .currentUser) // observers will be notified that currentUser changed even if currentUser was nil before +entityStore.store(newCurrentUser, named: .currentUser) // observers will be notified that currentUser changed even if currentUser was nil before ``` ### Stale data @@ -268,43 +268,43 @@ When storing data CohesionKit actually require you to set a modification stamp o By default CohesionKit will use the current date as stamp. ```swift -identityMap.store(book) // use default stamp: current date -identityMap.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp -identityMap.store(book, modifiedAt: 9000) // any Double value is valid +entityStore.store(book) // use default stamp: current date +entityStore.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp +entityStore.store(book, modifiedAt: 9000) // any Double value is valid ``` If for some reason you try to store data with a stamp lower than the already stamped stored data then the update will be discarded. ### Weak memory management -CohesionKit has a weak memory policy: objects are kept in `IdentityMap` as long as someone use them. +CohesionKit has a weak memory policy: objects are kept in `EntityStore` as long as someone use them. To that end you need to retain observers as long as you're interested in the data: ```swift let book = Book(id: "ACK", title: "A Clash of Kings") -let cancellable = identityMap.store(book) // observer is retained: data is retained +let cancellable = entityStore.store(book) // observer is retained: data is retained -identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings" +entityStore.find(Book.self, id: "ACK") // return "A Clash of Kings" ``` If you don't create/retain observers then once entities have no more observers they will be automatically discarded from the storage. ```swift let book = Book(id: "ACK", title: "A Clash of Kings") -_ = identityMap.store(book) // observer is not retained and no one else observe this book: data is released +_ = entityStore.store(book) // observer is not retained and no one else observe this book: data is released -identityMap.find(Book.self, id: "ACK") // return nil +entityStore.find(Book.self, id: "ACK") // return nil ``` ```swift let book = Book(id: "ACK", title: "A Clash of Kings") -var cancellable = identityMap.store(book).asPublisher.sink { ... } -let cancellable2 = identityMap.find(Book.self, id: "ACK") // return a publisher +var cancellable = entityStore.store(book).asPublisher.sink { ... } +let cancellable2 = entityStore.find(Book.self, id: "ACK") // return a publisher cancellable = nil -identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings" because cancellable2 still observe this book +entityStore.find(Book.self, id: "ACK") // return "A Clash of Kings" because cancellable2 still observe this book ``` # License diff --git a/Sources/CohesionKit/Combine/Publisher+CohesionKit.swift b/Sources/CohesionKit/Combine/Publisher+CohesionKit.swift index 38c0770..d872ad4 100644 --- a/Sources/CohesionKit/Combine/Publisher+CohesionKit.swift +++ b/Sources/CohesionKit/Combine/Publisher+CohesionKit.swift @@ -3,34 +3,34 @@ import Combine import Foundation extension Publisher { - /// Stores the `Identifiable` upstream into an identityMap - public func store(in identityMap: IdentityMap, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) + /// Stores the `Identifiable` upstream into an entityStore + public func store(in entityStore: EntityStore, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) -> AnyPublisher where Output: Identifiable { - map { identityMap.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher } + map { entityStore.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher } .switchToLatest() .eraseToAnyPublisher() } - /// Stores the `Aggregate` upstream into an identityMap - public func store(in identityMap: IdentityMap, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) + /// Stores the `Aggregate` upstream into an entityStore + public func store(in entityStore: EntityStore, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) -> AnyPublisher where Output: Aggregate { - map { identityMap.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher } + map { entityStore.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher } .switchToLatest() .eraseToAnyPublisher() } - /// Stores the upstream collection into an identityMap - public func store(in identityMap: IdentityMap, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) + /// Stores the upstream collection into an entityStore + public func store(in entityStore: EntityStore, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) -> AnyPublisher<[Output.Element], Failure> where Output: Collection, Output.Element: Identifiable { - map { identityMap.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher } + map { entityStore.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher } .switchToLatest() .eraseToAnyPublisher() } - /// Stores the upstream collection into an identityMap - public func store(in identityMap: IdentityMap, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) + /// Stores the upstream collection into an entityStore + public func store(in entityStore: EntityStore, named: AliasKey? = nil, modifiedAt: Stamp = Date().stamp) -> AnyPublisher<[Output.Element], Failure> where Output: Collection, Output.Element: Aggregate { - map { identityMap.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher } + map { entityStore.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher } .switchToLatest() .eraseToAnyPublisher() } diff --git a/Sources/CohesionKit/EntityStore.swift b/Sources/CohesionKit/EntityStore.swift index 48bc659..20c2480 100644 --- a/Sources/CohesionKit/EntityStore.swift +++ b/Sources/CohesionKit/EntityStore.swift @@ -1,7 +1,10 @@ import Foundation +@available(*, deprecated, renamed: "EntityStore") +public typealias IdentityMap = EntityStore + /// Manages entities lifecycle and synchronisation -public class IdentityMap { +public class EntityStore { public typealias Update = (inout T) -> Void /// the queue on which identity map do its heavy work @@ -11,9 +14,9 @@ public class IdentityMap { private(set) var storage: EntitiesStorage = EntitiesStorage() private(set) var refAliases: AliasStorage = [:] - private lazy var storeVisitor = IdentityMapStoreVisitor(identityMap: self) + private lazy var storeVisitor = EntityStoreStoreVisitor(entityStore: self) - /// Create a new IdentityMap instance optionally with a queue and a logger + /// Create a new EntityStore instance optionally with a queue and a logger /// - Parameter queue: the queue on which to receive updates. If nil identitymap will create its own. /// - Parameter logger: a logger to follow/debug identity internal state public convenience init(queue: DispatchQueue? = nil, logger: Logger? = nil) { @@ -217,7 +220,7 @@ public class IdentityMap { // MARK: Update -extension IdentityMap { +extension EntityStore { /// Updates an **already stored** entity using a closure. Useful to update a few properties or when you assume the entity /// should already be stored. /// Note: the closure is evaluated before checking `modifiedAt`. As such the closure execution does not mean @@ -341,7 +344,7 @@ extension IdentityMap { // MARK: Delete -extension IdentityMap { +extension EntityStore { /// Removes an alias from the storage public func removeAlias(named: AliasKey) { refAliases[named] = nil diff --git a/Sources/CohesionKit/Logger.swift b/Sources/CohesionKit/Logger.swift index f074240..f87df4b 100644 --- a/Sources/CohesionKit/Logger.swift +++ b/Sources/CohesionKit/Logger.swift @@ -1,17 +1,17 @@ import Foundation -/// a protocol reporting `IdentityMap` internal information +/// a protocol reporting `EntityStore` internal information public protocol Logger { /// Notify when an entity was stored in the identity map /// - Parameter type: the entity type /// - Parameter id: id of the stored entity func didStore(_ type: T.Type, id: T.ID) - + func didFailedToStore(_ type: T.Type, id: T.ID, error: Error) - + /// Notify an alias is registered with new entities func didRegisterAlias(_ alias: AliasKey) - + /// Notify an alias is suppressed from the identity map func didUnregisterAlias(_ alias: AliasKey) } diff --git a/Sources/CohesionKit/Stamp/Date+Stamp.swift b/Sources/CohesionKit/Stamp/Date+Stamp.swift index 1ac2cb5..1db5b9f 100644 --- a/Sources/CohesionKit/Stamp/Date+Stamp.swift +++ b/Sources/CohesionKit/Stamp/Date+Stamp.swift @@ -5,7 +5,7 @@ import Foundation public typealias Stamp = Double extension Date { - /// Generate a stamp suitable to use in `IdentityMap`. + /// Generate a stamp suitable to use in `EntityStore`. /// Don't suppose it equals unix timestamp (it is not) public var stamp: Stamp { timeIntervalSinceReferenceDate diff --git a/Sources/CohesionKit/Visitor/EntityContext.swift b/Sources/CohesionKit/Visitor/EntityContext.swift index ab7ad94..f5cd705 100644 --- a/Sources/CohesionKit/Visitor/EntityContext.swift +++ b/Sources/CohesionKit/Visitor/EntityContext.swift @@ -1,6 +1,6 @@ import Foundation -/// Information related to an entity while storing it into the `IdentityMap` +/// Information related to an entity while storing it into the `EntityStore` struct EntityContext { let parent: EntityNode let keyPath: WritableKeyPath diff --git a/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift b/Sources/CohesionKit/Visitor/EntityStoreVisitor.swift similarity index 75% rename from Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift rename to Sources/CohesionKit/Visitor/EntityStoreVisitor.swift index 7e94782..5b91287 100644 --- a/Sources/CohesionKit/Visitor/IdentityMapStoreVisitor.swift +++ b/Sources/CohesionKit/Visitor/EntityStoreVisitor.swift @@ -1,11 +1,11 @@ import Foundation -/// Visitor storing entity nested keypaths into IdentityMap -struct IdentityMapStoreVisitor: NestedEntitiesVisitor { - let identityMap: IdentityMap +/// Visitor storing entity nested keypaths into EntityStore +struct EntityStoreStoreVisitor: NestedEntitiesVisitor { + let entityStore: EntityStore func visit(context: EntityContext, entity: T) { - let storedChild = identityMap.nodeStore(entity: entity, modifiedAt: context.stamp) + let storedChild = entityStore.nodeStore(entity: entity, modifiedAt: context.stamp) context .parent @@ -13,7 +13,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { } func visit(context: EntityContext, entity: T) { - let storedChild = identityMap.nodeStore(entity: entity, modifiedAt: context.stamp) + let storedChild = entityStore.nodeStore(entity: entity, modifiedAt: context.stamp) context .parent @@ -24,7 +24,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { if let entity = entity { context .parent - .observeChild(identityMap.nodeStore(entity: entity, modifiedAt: context.stamp), for: context.keyPath) + .observeChild(entityStore.nodeStore(entity: entity, modifiedAt: context.stamp), for: context.keyPath) } } @@ -32,7 +32,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { if let entity = entity { context .parent - .observeChild(identityMap.nodeStore(entity: entity, modifiedAt: context.stamp), for: context.keyPath) + .observeChild(entityStore.nodeStore(entity: entity, modifiedAt: context.stamp), for: context.keyPath) } } @@ -40,7 +40,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { where C.Element: Identifiable, C.Index: Hashable { for index in entities.indices { context.parent.observeChild( - identityMap.nodeStore(entity: entities[index], modifiedAt: context.stamp), + entityStore.nodeStore(entity: entities[index], modifiedAt: context.stamp), for: context.keyPath.appending(path: \.[index]) ) } @@ -51,7 +51,7 @@ struct IdentityMapStoreVisitor: NestedEntitiesVisitor { for index in entities.indices { context.parent.observeChild( - identityMap.nodeStore(entity: entities[index], modifiedAt: context.stamp), + entityStore.nodeStore(entity: entities[index], modifiedAt: context.stamp), for: context.keyPath.appending(path: \.[index]) ) } diff --git a/Tests/CohesionKitTests/EntityStoreTests.swift b/Tests/CohesionKitTests/EntityStoreTests.swift index 386df45..f404243 100644 --- a/Tests/CohesionKitTests/EntityStoreTests.swift +++ b/Tests/CohesionKitTests/EntityStoreTests.swift @@ -11,46 +11,46 @@ class EntityStoreTests: XCTestCase { optional: .init(id: 1), listNodes: [ListNodeFixture(id: 1)] ) - let identityMap = IdentityMap() + let entityStore = EntityStore() - withExtendedLifetime(identityMap.store(entity: entity)) { _ in - XCTAssertNotNil(identityMap.storage[SingleNodeFixture.self, id: 1]) - XCTAssertNotNil(identityMap.storage[OptionalNodeFixture.self, id: 1]) - XCTAssertNotNil(identityMap.storage[ListNodeFixture.self, id: 1]) + withExtendedLifetime(entityStore.store(entity: entity)) { _ in + XCTAssertNotNil(entityStore.storage[SingleNodeFixture.self, id: 1]) + XCTAssertNotNil(entityStore.storage[OptionalNodeFixture.self, id: 1]) + XCTAssertNotNil(entityStore.storage[ListNodeFixture.self, id: 1]) } } func test_storeAggregate_nestedEntityReplacedByNil_entityIsUpdated_aggregateEntityRemainsNil() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let nestedOptional = OptionalNodeFixture(id: 1) var root = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), optional: nestedOptional, listNodes: []) - withExtendedLifetime(identityMap.store(entity: root)) { + withExtendedLifetime(entityStore.store(entity: root)) { root.optional = nil - _ = identityMap.store(entity: root) - _ = identityMap.store(entity: nestedOptional) + _ = entityStore.store(entity: root) + _ = entityStore.store(entity: nestedOptional) - XCTAssertNotNil(identityMap.find(RootFixture.self, id: 1)) - XCTAssertNil(identityMap.find(RootFixture.self, id: 1)!.value.optional) + XCTAssertNotNil(entityStore.find(RootFixture.self, id: 1)) + XCTAssertNil(entityStore.find(RootFixture.self, id: 1)!.value.optional) } } /// check that removed relations do not trigger an update func test_storeAggregate_removeEntityFromNestedArray_removedEntityChange_aggregateArrayNotChanged() { - let identityMap = IdentityMap() + let entityStore = EntityStore() var entityToRemove = ListNodeFixture(id: 2) let nestedArray: [ListNodeFixture] = [entityToRemove, ListNodeFixture(id: 1)] var root = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), optional: OptionalNodeFixture(id: 1), listNodes: nestedArray) - withExtendedLifetime(identityMap.store(entity: root)) { + withExtendedLifetime(entityStore.store(entity: root)) { root.listNodes = Array(nestedArray[1...]) entityToRemove.key = "changed" - _ = identityMap.store(entity: root) - _ = identityMap.store(entity: entityToRemove) + _ = entityStore.store(entity: root) + _ = entityStore.store(entity: entityToRemove) - let storedRoot = identityMap.find(RootFixture.self, id: 1)!.value + let storedRoot = entityStore.find(RootFixture.self, id: 1)!.value XCTAssertFalse(storedRoot.listNodes.contains(entityToRemove)) XCTAssertFalse(storedRoot.listNodes.map(\.id).contains(entityToRemove.id)) @@ -58,36 +58,36 @@ class EntityStoreTests: XCTestCase { } func test_storeAggregate_nestedWrapperChanged_aggregateIsUpdated() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let root = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), optional: OptionalNodeFixture(id: 1), listNodes: [], enumWrapper: .single(SingleNodeFixture(id: 2))) let updatedValue = SingleNodeFixture(id: 2, primitive: "updated") - withExtendedLifetime(identityMap.store(entity: root)) { - _ = identityMap.store(entity: updatedValue) - XCTAssertEqual(identityMap.find(RootFixture.self, id: 1)!.value.enumWrapper, .single(updatedValue)) + withExtendedLifetime(entityStore.store(entity: root)) { + _ = entityStore.store(entity: updatedValue) + XCTAssertEqual(entityStore.find(RootFixture.self, id: 1)!.value.enumWrapper, .single(updatedValue)) } } func test_storeAggregate_nestedOptionalWrapperNullified_aggregateIsNullified() { - let identityMap = IdentityMap() + let entityStore = EntityStore() var root = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), optional: OptionalNodeFixture(id: 1), listNodes: [], enumWrapper: .single(SingleNodeFixture(id: 2))) - withExtendedLifetime(identityMap.store(entity: root)) { + withExtendedLifetime(entityStore.store(entity: root)) { root.enumWrapper = nil - _ = identityMap.store(entity: root) - _ = identityMap.store(entity: SingleNodeFixture(id: 2, primitive: "deleted")) + _ = entityStore.store(entity: root) + _ = entityStore.store(entity: SingleNodeFixture(id: 2, primitive: "deleted")) - XCTAssertNil(identityMap.find(RootFixture.self, id: 1)!.value.enumWrapper) + XCTAssertNil(entityStore.find(RootFixture.self, id: 1)!.value.enumWrapper) } } func test_storeAggregate_registryContainsModifiedEntities() { let registryStub = ObserverRegistryStub(queue: .main) - let identityMap = IdentityMap(registry: registryStub) + let entityStore = EntityStore(registry: registryStub) let root = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), optional: OptionalNodeFixture(id: 1), listNodes: [], enumWrapper: .single(SingleNodeFixture(id: 2))) - withExtendedLifetime(identityMap.store(entity: root)) { + withExtendedLifetime(entityStore.store(entity: root)) { XCTAssertTrue(registryStub.hasPendingChange(for: root)) XCTAssertTrue(registryStub.hasPendingChange(for: SingleNodeFixture(id: 1))) XCTAssertTrue(registryStub.hasPendingChange(for: SingleNodeFixture(id: 2))) @@ -98,9 +98,9 @@ class EntityStoreTests: XCTestCase { func test_storeAggregate_named_itEnqueuesAliasInRegistry() { let root = SingleNodeFixture(id: 1) let registry = ObserverRegistryStub() - let identityMap = IdentityMap(registry: registry) + let entityStore = EntityStore(registry: registry) - withExtendedLifetime(identityMap.store(entity: root, named: .test)) { + withExtendedLifetime(entityStore.store(entity: root, named: .test)) { XCTAssertTrue(registry.hasPendingChange(for: AliasContainer.self)) XCTAssertTrue(registry.hasPendingChange(for: SingleNodeFixture.self)) } @@ -112,25 +112,25 @@ extension EntityStoreTests { /// make sure when inserting multiple time the same entity that it actually gets inserted only once func test_storeEntities_sameEntityPresentMultipleTimes_itIsInsertedOnce() { let registry = ObserverRegistryStub(queue: .main) - let identityMap = IdentityMap(registry: registry) + let entityStore = EntityStore(registry: registry) let commonEntity = SingleNodeFixture(id: 1) let root1 = RootFixture(id: 1, primitive: "", singleNode: commonEntity, optional: OptionalNodeFixture(id: 1), listNodes: [], enumWrapper: .single(SingleNodeFixture(id: 2))) let root2 = RootFixture(id: 1, primitive: "", singleNode: commonEntity, optional: OptionalNodeFixture(id: 1), listNodes: [], enumWrapper: nil) - _ = identityMap.store(entities: [root1, root2]) + _ = entityStore.store(entities: [root1, root2]) XCTAssertEqual(registry.pendingChangeCount(for: commonEntity), 1) } func test_storeEntities_named_calledMultipleTimes_lastValueIsStored() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let root = SingleNodeFixture(id: 1) let root2 = SingleNodeFixture(id: 2) - _ = identityMap.store(entities: [root], named: .listOfNodes) - _ = identityMap.store(entities: [root, root2], named: .listOfNodes) + _ = entityStore.store(entities: [root], named: .listOfNodes) + _ = entityStore.store(entities: [root, root2], named: .listOfNodes) - XCTAssertEqual(identityMap.find(named: .listOfNodes).value, [root, root2]) + XCTAssertEqual(entityStore.find(named: .listOfNodes).value, [root, root2]) } } @@ -138,7 +138,7 @@ extension EntityStoreTests { extension EntityStoreTests { func test_storeIdentifiable_entityIsInsertedForThe1stTime_loggerIsCalled() { let logger = LoggerMock() - let identityMap = IdentityMap(logger: logger) + let entityStore = EntityStore(logger: logger) let root = SingleNodeFixture(id: 1) let expectation = XCTestExpectation() @@ -150,18 +150,18 @@ extension EntityStoreTests { XCTFail() } - _ = identityMap.store(entity: root) + _ = entityStore.store(entity: root) wait(for: [expectation], timeout: 0.5) } func test_storeIdentifiable_entityIsAlreadyStored_updateIsCalled() { let root = SingleNodeFixture(id: 1) - let identityMap = IdentityMap() + let entityStore = EntityStore() let expectation = XCTestExpectation() - withExtendedLifetime(identityMap.store(entity: root)) { - _ = identityMap.store(entity: root, ifPresent: { _ in + withExtendedLifetime(entityStore.store(entity: root)) { + _ = entityStore.store(entity: root, ifPresent: { _ in expectation.fulfill() }) } @@ -173,52 +173,52 @@ extension EntityStoreTests { // MARK: Find extension EntityStoreTests { func test_find_entityStored_noObserverAdded_returnNil() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) - _ = identityMap.store(entity: entity) + _ = entityStore.store(entity: entity) - XCTAssertNil(identityMap.find(SingleNodeFixture.self, id: 1)) + XCTAssertNil(entityStore.find(SingleNodeFixture.self, id: 1)) } func test_find_entityStored_observedAdded_subscriptionIsReleased_returnNil() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) // don't keep a direct ref to EntityObserver to check memory release management - _ = identityMap.store(entity: entity).observe { _ in } + _ = entityStore.store(entity: entity).observe { _ in } - XCTAssertNil(identityMap.find(SingleNodeFixture.self, id: 1)) + XCTAssertNil(entityStore.find(SingleNodeFixture.self, id: 1)) } func test_find_entityStored_observerAdded_returnEntity() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) - withExtendedLifetime(identityMap.store(entity: entity).observe { _ in }) { - XCTAssertEqual(identityMap.find(SingleNodeFixture.self, id: 1)?.value, entity) + withExtendedLifetime(entityStore.store(entity: entity).observe { _ in }) { + XCTAssertEqual(entityStore.find(SingleNodeFixture.self, id: 1)?.value, entity) } } func test_find_entityStored_entityUpdatedByAnAggregate_returnUpdatedEntity() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) let update = SingleNodeFixture(id: 1, primitive: "Updated by Aggregate") - let subscription = identityMap.store(entity: entity).observe { _ in } + let subscription = entityStore.store(entity: entity).observe { _ in } withExtendedLifetime(subscription) { - _ = identityMap.store(entity: RootFixture(id: 1, primitive: "", singleNode: update, listNodes: [])) + _ = entityStore.store(entity: RootFixture(id: 1, primitive: "", singleNode: update, listNodes: [])) - XCTAssertEqual(identityMap.find(SingleNodeFixture.self, id: 1)?.value, update) + XCTAssertEqual(entityStore.find(SingleNodeFixture.self, id: 1)?.value, update) } } func test_find_entityStored_aggregateUpdateEntity_observerReturnUpdatedValue() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) let update = SingleNodeFixture(id: 1, primitive: "Updated by Aggregate") - let insertion = identityMap.store(entity: entity) + let insertion = entityStore.store(entity: entity) var firstDropped = false let subscription = insertion.observe { @@ -231,64 +231,64 @@ extension EntityStoreTests { } withExtendedLifetime(subscription) { - _ = identityMap.store(entity: RootFixture(id: 1, primitive: "", singleNode: update, listNodes: [])) + _ = entityStore.store(entity: RootFixture(id: 1, primitive: "", singleNode: update, listNodes: [])) } } func test_find_storedByAliasCollection_itReturnsEntity() { - let identityMap = IdentityMap() + let entityStore = EntityStore() - _ = identityMap.store(entities: [SingleNodeFixture(id: 1)], named: .listOfNodes) + _ = entityStore.store(entities: [SingleNodeFixture(id: 1)], named: .listOfNodes) - XCTAssertNotNil(identityMap.find(SingleNodeFixture.self, id: 1)) + XCTAssertNotNil(entityStore.find(SingleNodeFixture.self, id: 1)) } func test_find_storedByAliasAggregate_itReturnsEntity() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let aggregate = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), listNodes: []) - _ = identityMap.store(entity: aggregate, named: .root) + _ = entityStore.store(entity: aggregate, named: .root) - XCTAssertNotNil(identityMap.find(SingleNodeFixture.self, id: 1)) + XCTAssertNotNil(entityStore.find(SingleNodeFixture.self, id: 1)) } func test_findNamed_entityStored_noObserver_returnValue() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) - _ = identityMap.store(entity: entity, named: .test) + _ = entityStore.store(entity: entity, named: .test) - XCTAssertEqual(identityMap.find(named: .test).value, entity) + XCTAssertEqual(entityStore.find(named: .test).value, entity) } func test_findNamed_allAliasRemoved_returnNil() { - let identityMap = IdentityMap(queue: .main) + let entityStore = EntityStore(queue: .main) - _ = identityMap.store(entity: SingleNodeFixture(id: 1), named: .test, modifiedAt: 0) + _ = entityStore.store(entity: SingleNodeFixture(id: 1), named: .test, modifiedAt: 0) - XCTAssertNotNil(identityMap.find(named: .test).value) + XCTAssertNotNil(entityStore.find(named: .test).value) - identityMap.removeAllAlias() + entityStore.removeAllAlias() - XCTAssertNil(identityMap.find(named: .test).value) + XCTAssertNil(entityStore.find(named: .test).value) } func test_findNamed_entityStored_thenRemoved_returnNil() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) - _ = identityMap.store(entity: entity, named: .test) - identityMap.removeAlias(named: .test) + _ = entityStore.store(entity: entity, named: .test) + entityStore.removeAlias(named: .test) - XCTAssertNil(identityMap.find(named: .test).value) + XCTAssertNil(entityStore.find(named: .test).value) } func test_findNamed_aliasIsACollection_returnEntities() { - let identityMap = IdentityMap() + let entityStore = EntityStore() - _ = identityMap.store(entities: [SingleNodeFixture(id: 1)], named: .listOfNodes) + _ = entityStore.store(entities: [SingleNodeFixture(id: 1)], named: .listOfNodes) - XCTAssertNotNil(identityMap.find(named: .listOfNodes).value) + XCTAssertNotNil(entityStore.find(named: .listOfNodes).value) } } @@ -297,27 +297,27 @@ extension EntityStoreTests { extension EntityStoreTests { func test_update_entityIsAlreadyInserted_entityIsUpdated() { - let identityMap = IdentityMap() + let entityStore = EntityStore() let entity = SingleNodeFixture(id: 1) - withExtendedLifetime(identityMap.store(entity: entity)) { _ in - identityMap.update(SingleNodeFixture.self, id: 1) { + withExtendedLifetime(entityStore.store(entity: entity)) { _ in + entityStore.update(SingleNodeFixture.self, id: 1) { $0.primitive = "hello" } - XCTAssertEqual(identityMap.find(SingleNodeFixture.self, id: 1)?.value.primitive, "hello") + XCTAssertEqual(entityStore.find(SingleNodeFixture.self, id: 1)?.value.primitive, "hello") } } func test_updateNamed_entityIsIdentifiable_aliasIsExisting_observersAreNotified() { - let identityMap = IdentityMap(queue: .main) + let entityStore = EntityStore(queue: .main) let newEntity = SingleNodeFixture(id: 2) let expectation = XCTestExpectation() var firstDropped = false - _ = identityMap.store(entity: SingleNodeFixture(id: 1), named: .test, modifiedAt: 0) + _ = entityStore.store(entity: SingleNodeFixture(id: 1), named: .test, modifiedAt: 0) - let subscription = identityMap.find(named: .test).observe { + let subscription = entityStore.find(named: .test).observe { guard firstDropped else { firstDropped = true return @@ -328,7 +328,7 @@ extension EntityStoreTests { } withExtendedLifetime(subscription) { - identityMap.update(named: .test, modifiedAt: 1) { + entityStore.update(named: .test, modifiedAt: 1) { $0 = newEntity } @@ -337,14 +337,14 @@ extension EntityStoreTests { } func test_updateNamed_entityIsCollection_aliasIsExisting_observersAreNotified() { - let identityMap = IdentityMap(queue: .main) + let entityStore = EntityStore(queue: .main) let entities = [SingleNodeFixture(id: 1)] let expectation = XCTestExpectation() var firstDropped = false - _ = identityMap.store(entities: [], named: .listOfNodes, modifiedAt: 0) + _ = entityStore.store(entities: [], named: .listOfNodes, modifiedAt: 0) - let subscription = identityMap.find(named: .listOfNodes).observe { + let subscription = entityStore.find(named: .listOfNodes).observe { guard firstDropped else { firstDropped = true return @@ -355,7 +355,7 @@ extension EntityStoreTests { } withExtendedLifetime(subscription) { - identityMap.update(named: .listOfNodes, modifiedAt: 1) { + entityStore.update(named: .listOfNodes, modifiedAt: 1) { $0.append(contentsOf: entities) } @@ -365,7 +365,7 @@ extension EntityStoreTests { func test_updateNamed_entityIsAggregate_itEnqueuesNestedObjectsInRegistry() { let registry = ObserverRegistryStub() - let identityMap = IdentityMap(registry: registry) + let entityStore = EntityStore(registry: registry) let initialValue = RootFixture( id: 1, primitive: "", @@ -374,11 +374,11 @@ extension EntityStoreTests { ) let singleNodeUpdate = SingleNodeFixture(id: 1, primitive: "update") - _ = identityMap.store(entity: initialValue, named: .root) + _ = entityStore.store(entity: initialValue, named: .root) registry.clearPendingChangesStub() - identityMap.update(named: .root) { + entityStore.update(named: .root) { $0.singleNode = singleNodeUpdate } @@ -387,7 +387,7 @@ extension EntityStoreTests { func test_updateNamed_aliasIsAggregate_itEnqueuesAliasInRegistry() { let registry = ObserverRegistryStub() - let identityMap = IdentityMap(registry: registry) + let entityStore = EntityStore(registry: registry) let initialValue = RootFixture( id: 1, primitive: "", @@ -396,11 +396,11 @@ extension EntityStoreTests { ) let singleNodeUpdate = SingleNodeFixture(id: 1, primitive: "update") - _ = identityMap.store(entity: initialValue, named: .root) + _ = entityStore.store(entity: initialValue, named: .root) registry.clearPendingChangesStub() - identityMap.update(named: .root) { + entityStore.update(named: .root) { $0.singleNode = singleNodeUpdate } @@ -410,13 +410,13 @@ extension EntityStoreTests { func test_update_entityIsIndirectlyUsedByAlias_itEnqueuesAliasInRegistry() { let aggregate = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), listNodes: []) let registry = ObserverRegistryStub() - let identityMap = IdentityMap(registry: registry) + let entityStore = EntityStore(registry: registry) - _ = identityMap.store(entities: [aggregate], named: .rootList) + _ = entityStore.store(entities: [aggregate], named: .rootList) registry.clearPendingChangesStub() - identityMap.update(SingleNodeFixture.self, id: 1) { + entityStore.update(SingleNodeFixture.self, id: 1) { $0.primitive = "updated" } diff --git a/Tests/CohesionKitTests/Visitor/IdentityMapStoreVisitorTests.swift b/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift similarity index 91% rename from Tests/CohesionKitTests/Visitor/IdentityMapStoreVisitorTests.swift rename to Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift index c3a6447..e2e9984 100644 --- a/Tests/CohesionKitTests/Visitor/IdentityMapStoreVisitorTests.swift +++ b/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import CohesionKit -class IdentityMapStoreVisitorTests: XCTestCase { +class EntityStoreStoreVisitorTests: XCTestCase { let parent = RootFixture(id: 1, primitive: "", singleNode: .init(id: 1), optional: nil, listNodes: [.init(id: 2)]) private var parentNode: EntityNodeStub! @@ -20,7 +20,7 @@ class IdentityMapStoreVisitorTests: XCTestCase { XCTAssertTrue(type(of: keyPath).valueType == ListNodeFixture.self) } - IdentityMapStoreVisitor(identityMap: IdentityMap()) + EntityStoreStoreVisitor(entityStore: EntityStore()) .visit(context: context, entities: collection) wait(for: [expectation], timeout: 0) @@ -35,7 +35,7 @@ class IdentityMapStoreVisitorTests: XCTestCase { expectation.fulfill() } - IdentityMapStoreVisitor(identityMap: IdentityMap()) + EntityStoreStoreVisitor(entityStore: EntityStore()) .visit(context: context, entity: .some(entity)) wait(for: [expectation], timeout: 0) @@ -51,7 +51,7 @@ class IdentityMapStoreVisitorTests: XCTestCase { expectation.fulfill() } - IdentityMapStoreVisitor(identityMap: IdentityMap()) + EntityStoreStoreVisitor(entityStore: EntityStore()) .visit(context: context, entity: nil) wait(for: [expectation], timeout: 0) From c12f3a26adeb77dee62db1e14bfde5af0fb6f209 Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 21 Dec 2023 16:41:13 +0100 Subject: [PATCH 2/3] [wrapper] renamed EntityEnumWrapper -> EntityWrapper --- README.md | 4 ++-- ...EntityEnumWrapper.swift => EntityWrapper.swift} | 14 +++++++------- .../KeyPath/PartialIdentifiableKeyPath.swift | 4 ++-- Tests/CohesionKitTests/RootFixture.swift | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) rename Sources/CohesionKit/Entity/{EntityEnumWrapper.swift => EntityWrapper.swift} (52%) diff --git a/README.md b/README.md index 3ce7cce..865307f 100644 --- a/README.md +++ b/README.md @@ -195,10 +195,10 @@ Sometimes both can be used but they each have a different purpose: ### Enum support -Starting with 0.13 library has support for enum types. Note that you'll need to conform to `EntityEnumWrapper` and provide computed getter/setter for each entity you'd like to store. +Starting with 0.13 library has support for enum types. Note that you'll need to conform to `EntityWrapper` and provide computed getter/setter for each entity you'd like to store. ```swift -enum MediaType: EntityEnumWrapper { +enum MediaType: EntityWrapper { case book(Book) case game(Game) case tvShow(TvShow) diff --git a/Sources/CohesionKit/Entity/EntityEnumWrapper.swift b/Sources/CohesionKit/Entity/EntityWrapper.swift similarity index 52% rename from Sources/CohesionKit/Entity/EntityEnumWrapper.swift rename to Sources/CohesionKit/Entity/EntityWrapper.swift index 56396bd..ad7537f 100644 --- a/Sources/CohesionKit/Entity/EntityEnumWrapper.swift +++ b/Sources/CohesionKit/Entity/EntityWrapper.swift @@ -1,17 +1,17 @@ -/// a type wrapping one or more Identifiable types. As the name indicates you should use this type **only** on enums: -/// this is the easiest way to extract types they are containing. If you facing a non enum case where you feel you need -/// this type: rethink about it ;) -public protocol EntityEnumWrapper { +/// A type wrapping one or more Identifiable types. +/// You should rarely need to use this type. However it can happens to have a non Aggregate object containing Identifiable +/// objects to group them (for consistency or naming). This is especially true with enum cases. +public protocol EntityWrapper { /// Entities contained by all cases relative to the parent container - /// - Returns: entities contained in the enum + /// - Returns: entities contained in the wrapper //// /// Example: //// ```swift - /// enum MyEnum: EntityEnumWrapper { + /// enum MyEnum: EntityWrapper { /// case a(A) /// case b(B) /// - /// // you would also need to create computed getter/setter for a and b + /// // note: you would also need to create computed getter/setter for a and b /// func wrappedEntitiesKeyPaths(relativeTo root: WritableKeyPath) -> [PartialIdentifiableKeyPath] { /// [.init(root.appending(\.a)), .init(root.appending(\.b))] /// } diff --git a/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift b/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift index e3a3933..e510df9 100644 --- a/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift +++ b/Sources/CohesionKit/KeyPath/PartialIdentifiableKeyPath.swift @@ -92,7 +92,7 @@ public struct PartialIdentifiableKeyPath { } } - public init(wrapper keyPath: WritableKeyPath) { + public init(wrapper keyPath: WritableKeyPath) { self.keyPath = keyPath self.accept = { parent, root, stamp, visitor in for wrappedKeyPath in root[keyPath: keyPath].wrappedEntitiesKeyPaths(relativeTo: keyPath) { @@ -101,7 +101,7 @@ public struct PartialIdentifiableKeyPath { } } - public init(wrapper keyPath: WritableKeyPath) { + public init(wrapper keyPath: WritableKeyPath) { self.keyPath = keyPath self.accept = { parent, root, stamp, visitor in if let wrapper = root[keyPath: keyPath] { diff --git a/Tests/CohesionKitTests/RootFixture.swift b/Tests/CohesionKitTests/RootFixture.swift index 2fa2ff3..7f353aa 100644 --- a/Tests/CohesionKitTests/RootFixture.swift +++ b/Tests/CohesionKitTests/RootFixture.swift @@ -34,7 +34,7 @@ struct ListNodeFixture: Identifiable, Equatable { var key = "" } - enum EnumFixture: Equatable, EntityEnumWrapper { + enum EnumFixture: Equatable, EntityWrapper { case single(SingleNodeFixture) var singleNode: SingleNodeFixture? { From 737fe94cfe8a122dde5dfa6de865dc0860d7b777 Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 21 Dec 2023 19:03:04 +0100 Subject: [PATCH 3/3] [wrapper] add deprecated alias --- Sources/CohesionKit/Entity/EntityWrapper.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/CohesionKit/Entity/EntityWrapper.swift b/Sources/CohesionKit/Entity/EntityWrapper.swift index ad7537f..f1ab57f 100644 --- a/Sources/CohesionKit/Entity/EntityWrapper.swift +++ b/Sources/CohesionKit/Entity/EntityWrapper.swift @@ -1,3 +1,6 @@ +@available(unavailable, renamed: "EntityWrapper") +public typealias EntityEnumWrapper = EntityWrapper + /// A type wrapping one or more Identifiable types. /// You should rarely need to use this type. However it can happens to have a non Aggregate object containing Identifiable /// objects to group them (for consistency or naming). This is especially true with enum cases.