Deprecating add function in favor of insert

This commit is contained in:
Joe Fabisevich
2022-11-03 15:53:51 -04:00
parent 6e67a0dcb6
commit 9bd809e710
10 changed files with 167 additions and 97 deletions

View File

@@ -43,14 +43,14 @@ final class ImagesController: ObservableObject {
/// Saves an image to the `Store` in memory and on disk.
/// - Parameter image: A `RemoteImage` to be saved.
func saveImage(image: RemoteImage) async throws {
try await self.$images.add(image)
try await self.$images.insert(image)
}
// This function is unused but I wanted to demonstrate how you can chain operations together
func saveImageAfterClearingCache(image: RemoteImage) async throws {
try await self.$images
.removeAll()
.add(image)
.insert(image)
.run()
}

View File

@@ -21,7 +21,7 @@ final class RichNotesController: ObservableObject {
// Profiling how fast the operation is, consider elevating this to the UI
let timeBeforeAction = Date().timeIntervalSince1970
try await self.$notes.add(items)
try await self.$notes.insert(items)
let timeAfterAction = Date().timeIntervalSince1970
print(timeBeforeAction, timeAfterAction, String(format: "%.5fs", timeAfterAction - timeBeforeAction))

View File

@@ -45,7 +45,7 @@ You can read a high level overview of Boutique below, but Boutique is also fully
We'll go through a high level overview of the `Store` below, but the `Store` is fully documented with context, use cases, and examples [here](https://mergesort.github.io/Boutique/documentation/boutique/using-stores/).
The entire surface area of the API for achieving full offline support and realtime model updates across your entire app is three methods, `.add()`, `.remove()`, and `.removeAll()`.
The entire surface area of the API for achieving full offline support and realtime model updates across your entire app is three methods, `.insert()`, `.remove()`, and `.removeAll()`.
```swift
// Create a Store ¹
@@ -54,17 +54,17 @@ let store = Store<Animal>(
cacheIdentifier: \.id
)
// Add an item to the Store ²
// Insert an item into the Store ²
let redPanda = Animal(id: "red_panda")
try await store.add(redPanda)
try await store.insert(redPanda)
// Remove an animal from the Store
try await store.remove(redPanda)
// Add two more animals to the Store
// Insert two more animals to the Store
let dog = Item(name: "dog")
let cat = Item(name: "cat")
try await store.add([dog, cat])
try await store.insert([dog, cat])
// You can read items directly
print(store.items) // Prints [dog, cat]
@@ -76,8 +76,8 @@ print(store.items) // Prints []
// You can even chain commands together
try await store
.add(dog)
.add(cat)
.insert(dog)
.insert(cat)
.run()
print(store.items) // Prints [dog, cat]
@@ -85,7 +85,7 @@ print(store.items) // Prints [dog, cat]
// This is a good way to clear stale cached data
try await store
.removeAll()
.add(redPanda)
.insert(redPanda)
.run()
print(store.items) // Prints [redPanda]
@@ -151,7 +151,7 @@ final class ImagesController: ObservableObject {
/// Saves an image to the `Store` in memory and on disk.
func saveImage(image: RemoteImage) async throws {
try await self.$images.add(image)
try await self.$images.insert(image)
}
/// Removes one image from the `Store` in memory and on disk.

View File

@@ -96,7 +96,7 @@ public struct AsyncStoredValue<Item: Codable & Equatable> {
/// Within Boutique the @Stored property wrapper works very similarly.
/// - Parameter value: The value to set @``AsyncStoredValue`` to.
public func set(_ value: Item) async throws {
try await self.cancellableBox.store.add(UniqueItem(value: value))
try await self.cancellableBox.store.insert(UniqueItem(value: value))
}
/// Resets the @``AsyncStoredValue`` to the default value.

View File

@@ -4,7 +4,7 @@ Property Wrappers that take the ``Store`` and make it magical. ✨
## Overview
In <doc:Using-Stores> discussed how to initialize a ``Store``, and how to subsequently use it to add and remove items from that ``Store``. All of the code treats the ``Store`` as an easier to use database, but what if we could remove that layer of abstraction?
In <doc:Using-Stores> discussed how to initialize a ``Store``, and how to subsequently use it to insert and remove items from that ``Store``. All of the code treats the ``Store`` as an easier to use database, but what if we could remove that layer of abstraction?
The promise of Boutique is that you work with regular Swift values and arrays, yet have your data persisted automatically. The @``Stored``, @``StoredValue`` and @``AsyncStoredValue`` property wrappers are what help Boutique deliver on that promise.
@@ -14,7 +14,7 @@ That's a lot of words to discuss how @``Stored`` works under the hood, but seein
## The @Stored Array
Below we have a `NotesController`, a data controller. It has common operations such as the ability to fetch our notes from an API, along with adding, removing, and clearing them in our app.
Below we have a `NotesController`, a data controller. It has common operations such as the ability to fetch our notes from an API, along with inserting, removing, and clearing them in our app.
```swift
final class NotesController: ObservableObject {
@@ -27,12 +27,12 @@ final class NotesController: ObservableObject {
func fetchNotesFromAPI() -> [Note] {
// Make an API call that fetches an array of notes from the server...
self.$notes.add(notes)
self.$notes.insert(notes)
}
func addNote(note: Note) {
// Make an API call that adds the note to the server...
try await self.$notes.add(note)
// Make an API call that inserts the note to the server...
try await self.$notes.insert(note)
}
func removeNote(note: Note) {
@@ -67,15 +67,15 @@ self.notes // The type of the `wrappedValue` is [Note]
self.$notes // The type of the `projectedValue` is Store<Note>
```
By exposing a ``Store`` as the `projectedValue` we're now able to call `self.$notes.add(note)` to add a note to the `notes` array, an array that is `@Published public private(set)`.
By exposing a ``Store`` as the `projectedValue` we're now able to call `self.$notes.insert(note)` to insert a note into the `notes` array, an array that is `@Published public private(set)`.
Since `items` is an `@Published` array that means every time the value is updated, the changes will propagate across our entire app. That's how Boutique achieves it's realtime updating, keeping your entire app in sync without any additional code.
Making the access control of `items` `public private(set)` makes the `notes` array read-only, letting us observe that array safely in our views.
```swift
self.notes.add(note) // Does not work because `notes` is a *read-only* array.
self.$notes.add(note) // Works because $notes represents a `Store`
self.notes.insert(note) // Does not work because `notes` is a *read-only* array.
self.$notes.insert(note) // Works because $notes represents a `Store`
```
## Observing a Store's values

View File

@@ -22,7 +22,7 @@ let store = Store<Item>(
- The `storage` parameter is populated with a `StorageEngine`, you can read more about it in [Bodega's StorageEngine documentation](https://mergesort.github.io/Bodega/documentation/bodega/using-storageengines). Our SQLite database will be created in the platform's default storage directory, nested in an `Items` subdirectory. On macOS this will be the `Application Support` directory, and on every other platform such as iOS this will be the `Documents` directory. If you need finer control over the location you can specify a `FileManager.Directory` such as `.documents`, `.caches`, `.temporary`, or even provide your own URL, also explored in [Bodega's StorageEngine documentation](https://mergesort.github.io/Bodega/documentation/bodega/using-storageengines).
- The `cacheIdentifier` is a `KeyPath<Model, String>` that your model must provide. That may seem unconventional at first, so let's break it down. Much like how protocols enforce a contract, the KeyPath is doing the same for our model. To be added to our ``Store`` and saved to disk our models must conform to `Codable & Equatable`, both of which are reasonable constraints given the data has to be serializable and searchable. But what we're trying to avoid is making our models have to conform to a specialized caching protocol, we want to be able to save any ol' object you already have in your app. Instead of creating a protocol like `Storable`, we instead ask the model to tell us how we can derive a unique string which will be used as a key when storing the item.
- The `cacheIdentifier` is a `KeyPath<Model, String>` that your model must provide. That may seem unconventional at first, so let's break it down. Much like how protocols enforce a contract, the KeyPath is doing the same for our model. To be inserted into our ``Store`` and saved to disk our models must conform to `Codable & Equatable`, both of which are reasonable constraints given the data has to be serializable and searchable. But what we're trying to avoid is making our models have to conform to a specialized caching protocol, we want to be able to save any ol' object you already have in your app. Instead of creating a protocol like `Storable`, we instead ask the model to tell us how we can derive a unique string which will be used as a key when storing the item.
If your model (in this case `Item`) already conforms to `Identifiable`, we can simplify our initializer by eschewing the `cacheIdentifier` parameter.
@@ -44,11 +44,11 @@ This is how simple it is to create a full database-backed persistence layer, onl
## How Are Items Stored?
We'll explore how to use `.add(item: Item)` to save items, but it's worth taking a minute to discuss how items are stored in the ``Store``. When an item is saved to a ``Store``, that item is added to an array of `items`, and it is also persisted by the `StorageEngine`. If we use `DiskStorageEngine` the item will be saved to a disk, or if we use `SQLiteStorageEngine` the item will be saved to a database. The items are saved to the directory specified in the `DiskStorageEngine` or `SQLiteStorageEngine` initializer, and each item will be stored uniquely based on it's `cacheIdentifier` key.
We'll explore how to use `.insert(item: Item)` to save items, but it's worth taking a minute to discuss how items are stored in the ``Store``. When an item is saved to a ``Store``, that item is added to an array of `items`, and it is also persisted by the `StorageEngine`. If we use `DiskStorageEngine` the item will be saved to a disk, or if we use `SQLiteStorageEngine` the item will be saved to a database. The items are saved to the directory specified in the `DiskStorageEngine` or `SQLiteStorageEngine` initializer, and each item will be stored uniquely based on it's `cacheIdentifier` key.
The `cacheIdentifier` provides a mechanism for disambiguating objects, guaranteeing uniqueness of the items in our ``Store``. You never have to think about whether the item needs to be added or inserted (overwriting a matching item), or what index to insert an item at. Since we have a `cacheIdentifier` for every item we will know when an item should be added or overwritten inside of the ``Store``. This behavior means the ``Store`` operates more like a `Set` than an `Array`, because we are adding items into a bag of objects, and don't care in what order.
The `cacheIdentifier` provides a mechanism for disambiguating objects, guaranteeing uniqueness of the items in our ``Store``. You never have to think about whether the item needs to be added or inserted (overwriting a matching item), or what index to insert an item at. Since we have a `cacheIdentifier` for every item we will know when an item should be added or overwritten inside of the ``Store``. This behavior means the ``Store`` operates more like a `Set` than an `Array`, because we are inserting items into a bag of objects, and don't care in what order.
As a result the only operations you have to know are `.add`, `.remove`, and `.removeAll`, all of which are explored in the **Store Operations** section below. If you do need to sort the items into a particular order, for example if you're displaying the items alphabetically, you can always use the `items` property of a ``Store`` and sort, filter, map, or transform it as you would any other array.
As a result the only operations you have to know are `.insert`, `.remove`, and `.removeAll`, all of which are explored in the **Store Operations** section below. If you do need to sort the items into a particular order, for example if you're displaying the items alphabetically, you can always use the `items` property of a ``Store`` and sort, filter, map, or transform it as you would any other array.
To see how this looks I would highly recommend checking out the [Boutique demo app](https://github.com/mergesort/Boutique/tree/main/Demo), as it shows off more complex examples of what Boutique and the Store can do. There's even an example of how to sort items in a View based on time of creation [here](https://github.com/mergesort/Boutique/blob/main/Demo/Demo/Components/FavoritesCarouselView.swift#L152-L154).
@@ -56,11 +56,11 @@ To see how this looks I would highly recommend checking out the [Boutique demo a
The `items` property of a ``Store`` has an access control of `public private (set)`, preventing the direct modification of the `items` array. If you want to mutate the `items` of a ``Store`` you need to use the three functions the ``Store`` exposes. The API surface area is very small though, there are only three functions you need to know.
Add an item to the ``Store``
Inserts an item into the ``Store``
```swift
let coat = Item(name: "coat")
try await store.add(coat)
try await store.insert(coat)
```
Remove an item from the ``Store``
@@ -80,7 +80,7 @@ You can even chain operations using the `.run()` function, executing them in the
```swift
try await store
.removeAll()
.add(coat)
.insert(coat)
.run()
```

View File

@@ -5,7 +5,7 @@ public extension Store {
/// An invalidation strategy for a `Store` instance.
///
/// An `ItemRemovalStrategy` provides control over how items are removed from the `Store`
/// and `StorageEngine` cache when you are adding new items to the `Store`.
/// and `StorageEngine` cache when you are inserting new items into the `Store`.
///
/// This type used to be used publicly but now it's only used internally. As a result you
/// can no longer construct your own strategies, only `.all` and `.items(_:)` remain.

View File

@@ -1,9 +1,9 @@
public extension Store {
/// An operation is a type that allows you to stack ``add(_:)-82sdc``,
/// An operation is a type that allows you to stack ``insert(_:)-7z2oe``,
/// ``remove(_:)-8ufsb``, or ``removeAll()-1xc24`` calls in a chained manner.
///
/// This allows for simple fluent syntax such as `store.removeAll().add(items)`, rather than having
/// This allows for simple fluent syntax such as `store.removeAll().insert(items)`, rather than having
/// them be split over two operations, and making two separate dispatches to the `@MainActor`.
/// (Dispatching to the main actor multiple times can lead to users seeing odd visual experiences
/// in SwiftUI apps, which is why Boutique goes to great lengths to help avoid that.)
@@ -17,58 +17,58 @@ public extension Store {
self.store = store
}
/// Adds an item to the ``Store``.
/// Inserts an item into the ``Store``.
///
/// When an item is inserted with the same `cacheIdentifier` as an item that already exists in the ``Store``
/// the item being inserted will replace the item in the ``Store``. You can think of the ``Store`` as a bag
/// of items, removing complexity when it comes to managing items, indices, and more,
/// but it also means you need to choose well thought out and uniquely identifying `cacheIdentifier`s.
/// - Parameters:
/// - item: The item you are adding to the ``Store``.
public func add(_ item: Item) async throws -> Operation {
/// - item: The item you are inserting into the ``Store``.
public func insert(_ item: Item) async throws -> Operation {
if case .removeItems(let removedItems) = self.operations.last?.action {
self.operations.removeLast()
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(item, firstRemovingExistingItems: .items(removedItems))
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(item, firstRemovingExistingItems: .items(removedItems))
}))
} else if case .removeAll = self.operations.last?.action {
self.operations.removeLast()
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(item, firstRemovingExistingItems: .all)
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(item, firstRemovingExistingItems: .all)
}))
} else {
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(item)
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(item)
}))
}
return self
}
/// Adds an array of items to the ``Store``.
/// Inserts an array of items to the ``Store``.
///
/// Prefer adding multiple items using this method instead of calling ``add(_:)-82sdc
/// Prefer inserting multiple items using this method instead of calling ``insert(_:)-1nu61``
/// multiple times to avoid making multiple separate dispatches to the `@MainActor`.
/// - Parameters:
/// - items: The items to add to the store.
public func add(_ items: [Item]) async throws -> Operation {
/// - items: The items to insert into the store.
public func insert(_ items: [Item]) async throws -> Operation {
if case .removeItems(let removedItems) = self.operations.last?.action {
self.operations.removeLast()
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(items, firstRemovingExistingItems: .items(removedItems))
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(items, firstRemovingExistingItems: .items(removedItems))
}))
} else if case .removeAll = self.operations.last?.action {
self.operations.removeLast()
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(items, firstRemovingExistingItems: .all)
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(items, firstRemovingExistingItems: .all)
}))
} else {
self.operations.append(ExecutableAction(action: .add, executable: {
try await $0.performAdd(items)
self.operations.append(ExecutableAction(action: .insert, executable: {
try await $0.performInsert(items)
}))
}
@@ -137,7 +137,7 @@ private extension Store.Operation {
}
enum Action {
case add
case insert
case removeItem(_ item: Item)
case removeItems(_ items: [Item])
case removeAll

View File

@@ -9,7 +9,7 @@ import Foundation
/// using `store.items`, or subscribe to `store.$items` reactively for real-time changes and updates.
///
/// Under the hood the ``Store`` is doing the work of saving all changes to a persistence layer
/// when you add or remove items, which allows you to build an offline-first app
/// when you insert or remove items, which allows you to build an offline-first app
/// for free, all inclusive, *no extra code required*.
///
/// **How The Store Works**
@@ -52,7 +52,7 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
///
/// The user can read the state of ``items`` at any time
/// or subscribe to it however they wish, but you desire making modifications to ``items``
/// you must use ``add(_:)-dfro``, ``remove(_:)-3nzlq``, or ``removeAll()-9zfmy``.
/// you must use ``insert(_:)-7z2oe``, ``remove(_:)-3nzlq``, or ``removeAll()-9zfmy``.
@MainActor @Published public private(set) var items: [Item] = []
/// Initializes a new ``Store`` for persisting items to a memory cache
@@ -87,9 +87,29 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
/// - item: The item you are adding to the ``Store``.
/// - Returns: An ``Operation`` that can be used to add an item as part of a chain.
@_disfavoredOverload
@available(
*, deprecated,
renamed: "insert",
message: "This method is functionally equivalent to `insert` and will be removed in v3. After using Boutique in practice for a while I decided that insert was a more semantically correct name for this operation on a Store, if you'd like to learn more you can see the discussion here. https://github.com/mergesort/Boutique/discussions/36"
)
public func add(_ item: Item) async throws -> Operation {
let operation = Operation(store: self)
return try await operation.add(item)
return try await operation.insert(item)
}
/// Inserts an item into the store.
///
/// When an item is inserted with the same `cacheIdentifier` as an item that already exists in the ``Store``
/// the item being inserted will replace the item in the ``Store``. You can think of the ``Store`` as a bag
/// of items, removing complexity when it comes to managing items, indices, and more,
/// but it also means you need to choose well thought out and uniquely identifying `cacheIdentifier`s.
/// - Parameters:
/// - item: The item you are inserting into the ``Store``.
/// - Returns: An ``Operation`` that can be used to insert an item as part of a chain.
@_disfavoredOverload
public func insert(_ item: Item) async throws -> Operation {
let operation = Operation(store: self)
return try await operation.insert(item)
}
/// Adds an item to the ``Store``.
@@ -100,8 +120,25 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
/// but it also means you need to choose well thought out and uniquely identifying `cacheIdentifier`s.
/// - Parameters:
/// - item: The item you are adding to the ``Store``.
@available(
*, deprecated,
renamed: "insert",
message: "This method is functionally equivalent to `insert` and will be removed in v3. After using Boutique in practice for a while I decided that insert was a more semantically correct name for this operation on a Store, if you'd like to learn more you can see the discussion here. https://github.com/mergesort/Boutique/discussions/36"
)
public func add(_ item: Item) async throws {
try await self.performAdd(item)
try await self.performInsert(item)
}
/// Inserts an item into the ``Store``.
///
/// When an item is inserted with the same `cacheIdentifier` as an item that already exists in the ``Store``
/// the item being inserted will replace the item in the ``Store``. You can think of the ``Store`` as a bag
/// of items, removing complexity when it comes to managing items, indices, and more,
/// but it also means you need to choose well thought out and uniquely identifying `cacheIdentifier`s.
/// - Parameters:
/// - item: The item you are inserting into the ``Store``.
public func insert(_ item: Item) async throws {
try await self.performInsert(item)
}
/// Adds an array of items to the ``Store``.
@@ -112,19 +149,52 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
/// - items: The items to add to the store.
/// - Returns: An ``Operation`` that can be used to add items as part of a chain.
@_disfavoredOverload
@available(
*, deprecated,
renamed: "insert",
message: "This method is functionally equivalent to `insert` and will be removed in v3. After using Boutique in practice for a while I decided that insert was a more semantically correct name for this operation on a Store, if you'd like to learn more you can see the discussion here. https://github.com/mergesort/Boutique/discussions/36"
)
public func add(_ items: [Item]) async throws -> Operation {
let operation = Operation(store: self)
return try await operation.add(items)
return try await operation.insert(items)
}
/// Inserts an array of items into the ``Store``.
///
/// Prefer inserting multiple items using this method instead of calling ``insert(_:)-7z2oe``
/// multiple times to avoid making multiple separate dispatches to the `@MainActor`.
/// - Parameters:
/// - items: The items to insert into the store.
/// - Returns: An ``Operation`` that can be used to insert items as part of a chain.
@_disfavoredOverload
public func insert(_ items: [Item]) async throws -> Operation {
let operation = Operation(store: self)
return try await operation.insert(items)
}
/// Adds an array of items to the ``Store``.
///
/// Prefer adding multiple items using this method instead of calling ``add(_:)-1np7h``
/// Prefer adding multiple items using this method instead of calling ``insert(_:)-7z2oe``
/// multiple times to avoid making multiple separate dispatches to the `@MainActor`.
/// - Parameters:
/// - items: The items to add to the store.
@available(
*, deprecated,
renamed: "insert",
message: "This method is functionally equivalent to `insert` and will be removed in v3. After using Boutique in practice for a while I decided that insert was a more semantically correct name for this operation on a Store, if you'd like to learn more you can see the discussion here. https://github.com/mergesort/Boutique/discussions/36"
)
public func add(_ items: [Item]) async throws {
try await self.performAdd(items)
try await self.performInsert(items)
}
/// Inserts an array of items into the ``Store``.
///
/// Prefer inserting multiple items using this method instead of calling ``insert(_:)-3j9hw``
/// multiple times to avoid making multiple separate dispatches to the `@MainActor`.
/// - Parameters:
/// - items: The items to insert into the store.
public func insert(_ items: [Item]) async throws {
try await self.performInsert(items)
}
/// Removes an item from the ``Store``.
@@ -218,10 +288,10 @@ public extension Store {
}
#endif
// Internal versions of the `add`, `remove`, and `removeAll` function code pths so we can avoid duplicating code.
// Internal versions of the `insert`, `remove`, and `removeAll` function code pths so we can avoid duplicating code.
internal extension Store {
func performAdd(_ item: Item, firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
func performInsert(_ item: Item, firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
var currentItems = await self.items
if let strategy = existingItemsStrategy {
@@ -243,7 +313,7 @@ internal extension Store {
}
}
func performAdd(_ items: [Item], firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
func performInsert(_ items: [Item], firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy<Item>? = nil) async throws {
var currentItems = await self.items
if let strategy = existingItemsStrategy {
@@ -251,13 +321,13 @@ internal extension Store {
try await self.removeItems(withStrategy: strategy, items: &currentItems)
}
var addedItemsDictionary = OrderedDictionary<String, Item>()
var insertedItemsDictionary = OrderedDictionary<String, Item>()
// Deduplicate items passed into `add(items:)` by taking advantage
// Deduplicate items passed into `insert(items:)` by taking advantage
// of the fact that an OrderedDictionary can't have duplicate keys.
for item in items {
let identifier = item[keyPath: self.cacheIdentifier]
addedItemsDictionary[identifier] = item
insertedItemsDictionary[identifier] = item
}
// Take the current items array and turn it into an OrderedDictionary.
@@ -265,13 +335,13 @@ internal extension Store {
var currentValuesDictionary = OrderedDictionary<String, Item>(uniqueKeys: currentItemsKeys, values: currentItems)
// Add the new items into the dictionary representation of our items.
for item in addedItemsDictionary {
for item in insertedItemsDictionary {
let identifier = item.value[keyPath: self.cacheIdentifier]
currentValuesDictionary[identifier] = item.value
}
// We persist only the newly added items, rather than rewriting all of the items
try await self.persistItems(Array(addedItemsDictionary.values))
try await self.persistItems(Array(insertedItemsDictionary.values))
await MainActor.run { [currentValuesDictionary] in
self.items = Array(currentValuesDictionary.values)

View File

@@ -16,33 +16,33 @@ final class StoreTests: XCTestCase {
}
@MainActor
func testAddingItem() async throws {
try await store.add(BoutiqueItem.coat)
func testInsertingItem() async throws {
try await store.insert(BoutiqueItem.coat)
XCTAssertTrue(store.items.contains(BoutiqueItem.coat))
try await store.add(BoutiqueItem.belt)
try await store.insert(BoutiqueItem.belt)
XCTAssertTrue(store.items.contains(BoutiqueItem.belt))
XCTAssertEqual(store.items.count, 2)
}
@MainActor
func testAddingItems() async throws {
try await store.add([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse])
func testInsertingItems() async throws {
try await store.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse])
XCTAssertTrue(store.items.contains(BoutiqueItem.coat))
XCTAssertTrue(store.items.contains(BoutiqueItem.sweater))
XCTAssertTrue(store.items.contains(BoutiqueItem.purse))
}
@MainActor
func testAddingDuplicateItems() async throws {
func testInsertingDuplicateItems() async throws {
XCTAssertTrue(store.items.isEmpty)
try await store.add(BoutiqueItem.allItems)
try await store.insert(BoutiqueItem.allItems)
XCTAssertEqual(store.items.count, 4)
}
@MainActor
func testReadingItems() async throws {
try await store.add(BoutiqueItem.allItems)
try await store.insert(BoutiqueItem.allItems)
XCTAssertEqual(store.items[0], BoutiqueItem.coat)
XCTAssertEqual(store.items[1], BoutiqueItem.sweater)
@@ -54,7 +54,7 @@ final class StoreTests: XCTestCase {
@MainActor
func testRemovingItems() async throws {
try await store.add(BoutiqueItem.allItems)
try await store.insert(BoutiqueItem.allItems)
try await store.remove(BoutiqueItem.coat)
XCTAssertFalse(store.items.contains(BoutiqueItem.coat))
@@ -69,24 +69,24 @@ final class StoreTests: XCTestCase {
@MainActor
func testRemoveAll() async throws {
try await store.add(BoutiqueItem.coat)
try await store.insert(BoutiqueItem.coat)
XCTAssertEqual(store.items.count, 1)
try await store.removeAll()
try await store.add(BoutiqueItem.uniqueItems)
try await store.insert(BoutiqueItem.uniqueItems)
XCTAssertEqual(store.items.count, 4)
try await store.removeAll()
XCTAssertTrue(store.items.isEmpty)
}
@MainActor
func testChainingAddOperations() async throws {
try await store.add(BoutiqueItem.uniqueItems)
func testChainingInsertOperations() async throws {
try await store.insert(BoutiqueItem.uniqueItems)
try await store
.remove(BoutiqueItem.coat)
.add(BoutiqueItem.belt)
.add(BoutiqueItem.belt)
.insert(BoutiqueItem.belt)
.insert(BoutiqueItem.belt)
.run()
XCTAssertEqual(store.items.count, 3)
@@ -98,10 +98,10 @@ final class StoreTests: XCTestCase {
try await store.removeAll()
try await store
.add(BoutiqueItem.belt)
.add(BoutiqueItem.coat)
.insert(BoutiqueItem.belt)
.insert(BoutiqueItem.coat)
.remove([BoutiqueItem.belt])
.add(BoutiqueItem.sweater)
.insert(BoutiqueItem.sweater)
.run()
XCTAssertEqual(store.items.count, 2)
@@ -110,11 +110,11 @@ final class StoreTests: XCTestCase {
XCTAssertFalse(store.items.contains(BoutiqueItem.belt))
try await store
.add(BoutiqueItem.belt)
.add(BoutiqueItem.coat)
.add(BoutiqueItem.purse)
.insert(BoutiqueItem.belt)
.insert(BoutiqueItem.coat)
.insert(BoutiqueItem.purse)
.remove([BoutiqueItem.belt, .coat])
.add(BoutiqueItem.sweater)
.insert(BoutiqueItem.sweater)
.run()
XCTAssertEqual(store.items.count, 2)
@@ -126,8 +126,8 @@ final class StoreTests: XCTestCase {
try await store.removeAll()
try await store
.add(BoutiqueItem.coat)
.add([BoutiqueItem.purse, BoutiqueItem.belt])
.insert(BoutiqueItem.coat)
.insert([BoutiqueItem.purse, BoutiqueItem.belt])
.run()
XCTAssertEqual(store.items.count, 3)
@@ -139,7 +139,7 @@ final class StoreTests: XCTestCase {
@MainActor
func testChainingRemoveOperations() async throws {
try await store
.add(BoutiqueItem.uniqueItems)
.insert(BoutiqueItem.uniqueItems)
.remove(BoutiqueItem.belt)
.remove(BoutiqueItem.purse)
.run()
@@ -148,7 +148,7 @@ final class StoreTests: XCTestCase {
XCTAssertTrue(store.items.contains(BoutiqueItem.sweater))
XCTAssertTrue(store.items.contains(BoutiqueItem.coat))
try await store.add(BoutiqueItem.uniqueItems)
try await store.insert(BoutiqueItem.uniqueItems)
XCTAssertEqual(store.items.count, 4)
try await store
@@ -161,7 +161,7 @@ final class StoreTests: XCTestCase {
try await store
.removeAll()
.add(BoutiqueItem.belt)
.insert(BoutiqueItem.belt)
.run()
XCTAssertEqual(store.items.count, 1)
@@ -170,7 +170,7 @@ final class StoreTests: XCTestCase {
try await store
.removeAll()
.remove(BoutiqueItem.belt)
.add(BoutiqueItem.belt)
.insert(BoutiqueItem.belt)
.run()
XCTAssertEqual(store.items.count, 1)
@@ -180,8 +180,8 @@ final class StoreTests: XCTestCase {
@MainActor
func testChainingOperationsDontExecuteUnlessRun() async throws {
let operation = try await store
.add(BoutiqueItem.coat)
.add([BoutiqueItem.purse, BoutiqueItem.belt])
.insert(BoutiqueItem.coat)
.insert([BoutiqueItem.purse, BoutiqueItem.belt])
XCTAssertEqual(store.items.count, 0)
XCTAssertFalse(store.items.contains(BoutiqueItem.purse))
@@ -209,7 +209,7 @@ final class StoreTests: XCTestCase {
XCTAssertTrue(store.items.isEmpty)
// Sets items under the hood
try await store.add(uniqueItems)
try await store.insert(uniqueItems)
wait(for: [expectation], timeout: 1)
}