Skip to content

Commit 75f15dc

Browse files
committed
Create DataStoreQuery type to perform query
1 parent 80797bc commit 75f15dc

File tree

6 files changed

+76
-66
lines changed

6 files changed

+76
-66
lines changed

WordPress/Classes/Services/UserService.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import WordPressUI
88
actor UserService: UserServiceProtocol, UserDataStoreProvider {
99
private let client: WordPressClient
1010

11-
private let _dataStore: InMemoryUserDataStore = .init()
12-
var userDataStore: any UserDataStore { _dataStore }
11+
private let _dataStore: InMemoryDataStore<DisplayUser> = .init()
12+
var userDataStore: any DataStore<DisplayUser> { _dataStore }
1313

1414
private var _currentUser: UserWithEditContext?
1515
private var currentUser: UserWithEditContext? {
@@ -51,7 +51,7 @@ actor UserService: UserServiceProtocol, UserDataStoreProvider {
5151

5252
// Remove the deleted user from the cached users list.
5353
if result.deleted {
54-
try await _dataStore.delete(query: .id([id]))
54+
try await _dataStore.delete(query: .identifier(in: [id]))
5555
}
5656
}
5757

Original file line numberDiff line numberDiff line change
@@ -1,33 +1,2 @@
11
import Foundation
22
import Combine
3-
4-
public actor InMemoryUserDataStore: UserDataStore, InMemoryDataStore {
5-
public typealias T = DisplayUser
6-
7-
public var storage: [T.ID: T] = [:]
8-
public let updates: PassthroughSubject<Set<T.ID>, Never> = .init()
9-
10-
deinit {
11-
updates.send(completion: .finished)
12-
}
13-
14-
public func list(query: Query) throws -> [T] {
15-
switch query {
16-
case .all:
17-
return Array(storage.values)
18-
case let .id(ids):
19-
return storage.reduce(into: []) {
20-
if ids.contains($1.key) {
21-
$0.append($1.value)
22-
}
23-
}
24-
case let .search(keyword):
25-
let theKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
26-
if theKeyword.isEmpty {
27-
return Array(storage.values)
28-
} else {
29-
return storage.values.search(theKeyword, using: \.searchString)
30-
}
31-
}
32-
}
33-
}

WordPress/Classes/Users/UserProvider.swift

+4-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import Foundation
22
import Combine
33

4-
public protocol UserDataStore: DataStore where T == DisplayUser, Query == UserDataStoreQuery {
5-
}
6-
7-
public enum UserDataStoreQuery: Equatable {
8-
case all
9-
case id(Set<DisplayUser.ID>)
10-
case search(String)
11-
}
12-
134
public protocol UserServiceProtocol: Actor {
145
func fetchUsers() async throws
156

@@ -27,7 +18,7 @@ public protocol UserServiceProtocol: Actor {
2718
}
2819

2920
protocol UserDataStoreProvider: Actor {
30-
var userDataStore: any UserDataStore { get }
21+
var userDataStore: any DataStore<DisplayUser> { get }
3122
}
3223

3324
extension UserServiceProtocol where Self: UserDataStoreProvider {
@@ -36,7 +27,7 @@ extension UserServiceProtocol where Self: UserDataStoreProvider {
3627
}
3728

3829
func streamSearchResult(input: String) async -> AsyncStream<Result<[DisplayUser], Error>> {
39-
await userDataStore.listStream(query: .search(input))
30+
await userDataStore.listStream(query: .search(input, transform: \.searchString))
4031
}
4132

4233
func streamAll() async -> AsyncStream<Result<[DisplayUser], Error>> {
@@ -54,8 +45,8 @@ actor MockUserProvider: UserServiceProtocol, UserDataStoreProvider {
5445

5546
var scenario: Scenario
5647

57-
private let _dataStore: InMemoryUserDataStore = .init()
58-
var userDataStore: any UserDataStore { _dataStore }
48+
private let _dataStore: InMemoryDataStore<DisplayUser> = .init()
49+
var userDataStore: any DataStore<DisplayUser> { _dataStore }
5950

6051
nonisolated let usersUpdates: AsyncStream<[DisplayUser]>
6152
private let usersUpdatesContinuation: AsyncStream<[DisplayUser]>.Continuation
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,60 @@
11
import Foundation
2+
import WordPressShared
23

34
/// An abstraction of local data storage, with CRUD operations.
4-
public protocol DataStore: Actor {
5-
associatedtype T: Identifiable & Sendable
6-
associatedtype Query
5+
public protocol DataStore<T>: Actor {
6+
associatedtype T: Identifiable & Sendable where T.ID: Sendable
77

8-
func list(query: Query) async throws -> [T]
9-
func delete(query: Query) async throws
8+
func list(query: DataStoreQuery<T>) async throws -> [T]
9+
func delete(query: DataStoreQuery<T>) async throws
1010
func store(_ data: [T]) async throws
1111

1212
/// An AsyncStream that produces up-to-date results for the given query.
1313
///
1414
/// The `AsyncStream` should not finish as long as the `DataStore` remains alive and valid.
15-
func listStream(query: Query) -> AsyncStream<Result<[T], Error>>
15+
func listStream(query: DataStoreQuery<T>) -> AsyncStream<Result<[T], Error>>
16+
}
17+
18+
public struct DataStoreQuery<T: Identifiable & Sendable>: Sendable where T.ID: Sendable {
19+
public indirect enum Filter: Sendable {
20+
case identifier(Set<T.ID>)
21+
case closure(@Sendable (T) -> Bool)
22+
case and(lhs: Filter, rhs: Filter)
23+
case or(lhs: Filter, rhs: Filter)
24+
25+
func evaluate(on value: T) -> Bool {
26+
switch self {
27+
case let .identifier(ids):
28+
ids.contains(value.id)
29+
case let .closure(closure):
30+
closure(value)
31+
case let .and(lhs, rhs):
32+
lhs.evaluate(on: value) && rhs.evaluate(on: value)
33+
case let .or(lhs, rhs):
34+
lhs.evaluate(on: value) || rhs.evaluate(on: value)
35+
}
36+
}
37+
}
38+
39+
var filter: Filter?
40+
var sortBy: [SortDescriptor<T>] = []
41+
42+
public func perform(on data: any Sequence<T>) -> [T] {
43+
var result: any Sequence<T> = data
44+
if let filter {
45+
result = result.filter { filter.evaluate(on: $0) }
46+
}
47+
return result.sorted(using: sortBy)
48+
}
49+
50+
public static var all: Self { .init() }
51+
52+
public static func identifier(in ids: Set<T.ID>) -> Self {
53+
.init(filter: .identifier(ids))
54+
}
55+
56+
public static func search(_ query: String, minScore: Double = 0.7, transform: @escaping (T) -> String) -> Self {
57+
let term = StringRankedSearch(searchTerm: query)
58+
return .init(filter: .closure { term.score(for: transform($0)) >= minScore })
59+
}
1660
}

WordPress/Classes/Utility/DataStore/InMemoryDataStore.swift

+15-9
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import Foundation
22
import Combine
33

44
/// A `DataStore` type that stores data in memory.
5-
public protocol InMemoryDataStore: DataStore {
5+
public actor InMemoryDataStore<T: Identifiable & Sendable>: DataStore where T.ID: Sendable {
66
/// A `Dictionary` to store the data in memory.
7-
var storage: [T.ID: T] { get set }
7+
var storage: [T.ID: T] = [:]
88

99
/// A publisher for sending and subscribing data changes.
1010
///
1111
/// The publisher emits events when data changes, with identifiers of changed models.
1212
///
1313
/// The publisher does not complete as long as the `InMemoryDataStore` remains alive and valid.
14-
var updates: PassthroughSubject<Set<T.ID>, Never> { get }
15-
}
14+
let updates: PassthroughSubject<Set<T.ID>, Never> = .init()
15+
16+
deinit {
17+
updates.send(completion: .finished)
18+
}
1619

17-
public extension InMemoryDataStore {
18-
func delete(query: Query) async throws {
20+
public func delete(query: DataStoreQuery<T>) async throws {
1921
var updated = Set<T.ID>()
20-
let result = try await list(query: query)
22+
let result = try list(query: query)
2123
result.forEach {
2224
if storage.removeValue(forKey: $0.id) != nil {
2325
updated.insert($0.id)
@@ -29,7 +31,7 @@ public extension InMemoryDataStore {
2931
}
3032
}
3133

32-
func store(_ data: [T]) async throws {
34+
public func store(_ data: [T]) async throws {
3335
var updated = Set<T.ID>()
3436
data.forEach {
3537
updated.insert($0.id)
@@ -41,7 +43,11 @@ public extension InMemoryDataStore {
4143
}
4244
}
4345

44-
func listStream(query: Query) -> AsyncStream<Result<[T], Error>> {
46+
public func list(query: DataStoreQuery<T>) throws -> [T] {
47+
query.perform(on: storage.values)
48+
}
49+
50+
public func listStream(query: DataStoreQuery<T>) -> AsyncStream<Result<[T], Error>> {
4551
let stream = AsyncStream<Result<[T], Error>>.makeStream()
4652

4753
let updatingTask = Task { [weak self] in

WordPress/WordPressTest/DataStoreTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ struct InMemoryDataStoreTests {
88

99
@Test
1010
func testUpdatesAfterCreation() async {
11-
let store: InMemoryUserDataStore = InMemoryUserDataStore()
11+
let store = InMemoryDataStore<DisplayUser>()
1212
let stream = await store.listStream(query: .all)
1313

1414
await confirmation("The stream produces an update") { confirmation in
@@ -20,7 +20,7 @@ struct InMemoryDataStoreTests {
2020

2121
@Test
2222
func testUpdatesAfterStore() async {
23-
let store: InMemoryUserDataStore = InMemoryUserDataStore()
23+
let store = InMemoryDataStore<DisplayUser>()
2424
let stream = await store.listStream(query: .all)
2525

2626
Task.detached {
@@ -37,7 +37,7 @@ struct InMemoryDataStoreTests {
3737

3838
@Test
3939
func testUpdatesAfterDelete() async throws {
40-
let store: InMemoryUserDataStore = InMemoryUserDataStore()
40+
let store = InMemoryDataStore<DisplayUser>()
4141
try await store.store([.MockUser])
4242

4343
let stream = await store.listStream(query: .all)
@@ -56,7 +56,7 @@ struct InMemoryDataStoreTests {
5656

5757
@Test
5858
func testStreamTerminates() async {
59-
var store: InMemoryUserDataStore? = InMemoryUserDataStore()
59+
var store: InMemoryDataStore<DisplayUser>? = .init()
6060
let stream = await store!.listStream(query: .all)
6161

6262
Task.detached {

0 commit comments

Comments
 (0)