Skip to content

Commit

Permalink
Create DataStoreQuery type to perform query
Browse files Browse the repository at this point in the history
  • Loading branch information
crazytonyli committed Nov 29, 2024
1 parent 80797bc commit 75f15dc
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 66 deletions.
6 changes: 3 additions & 3 deletions WordPress/Classes/Services/UserService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import WordPressUI
actor UserService: UserServiceProtocol, UserDataStoreProvider {
private let client: WordPressClient

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

private var _currentUser: UserWithEditContext?
private var currentUser: UserWithEditContext? {
Expand Down Expand Up @@ -51,7 +51,7 @@ actor UserService: UserServiceProtocol, UserDataStoreProvider {

// Remove the deleted user from the cached users list.
if result.deleted {
try await _dataStore.delete(query: .id([id]))
try await _dataStore.delete(query: .identifier(in: [id]))
}
}

Expand Down
31 changes: 0 additions & 31 deletions WordPress/Classes/Users/InMemoryUserDataStore.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,2 @@
import Foundation
import Combine

public actor InMemoryUserDataStore: UserDataStore, InMemoryDataStore {
public typealias T = DisplayUser

public var storage: [T.ID: T] = [:]
public let updates: PassthroughSubject<Set<T.ID>, Never> = .init()

deinit {
updates.send(completion: .finished)
}

public func list(query: Query) throws -> [T] {
switch query {
case .all:
return Array(storage.values)
case let .id(ids):
return storage.reduce(into: []) {
if ids.contains($1.key) {
$0.append($1.value)
}
}
case let .search(keyword):
let theKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
if theKeyword.isEmpty {
return Array(storage.values)
} else {
return storage.values.search(theKeyword, using: \.searchString)
}
}
}
}
17 changes: 4 additions & 13 deletions WordPress/Classes/Users/UserProvider.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import Foundation
import Combine

public protocol UserDataStore: DataStore where T == DisplayUser, Query == UserDataStoreQuery {
}

public enum UserDataStoreQuery: Equatable {
case all
case id(Set<DisplayUser.ID>)
case search(String)
}

public protocol UserServiceProtocol: Actor {
func fetchUsers() async throws

Expand All @@ -27,7 +18,7 @@ public protocol UserServiceProtocol: Actor {
}

protocol UserDataStoreProvider: Actor {
var userDataStore: any UserDataStore { get }
var userDataStore: any DataStore<DisplayUser> { get }
}

extension UserServiceProtocol where Self: UserDataStoreProvider {
Expand All @@ -36,7 +27,7 @@ extension UserServiceProtocol where Self: UserDataStoreProvider {
}

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

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

var scenario: Scenario

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

nonisolated let usersUpdates: AsyncStream<[DisplayUser]>
private let usersUpdatesContinuation: AsyncStream<[DisplayUser]>.Continuation
Expand Down
56 changes: 50 additions & 6 deletions WordPress/Classes/Utility/DataStore/DataStore.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
import Foundation
import WordPressShared

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

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

/// An AsyncStream that produces up-to-date results for the given query.
///
/// The `AsyncStream` should not finish as long as the `DataStore` remains alive and valid.
func listStream(query: Query) -> AsyncStream<Result<[T], Error>>
func listStream(query: DataStoreQuery<T>) -> AsyncStream<Result<[T], Error>>
}

public struct DataStoreQuery<T: Identifiable & Sendable>: Sendable where T.ID: Sendable {
public indirect enum Filter: Sendable {
case identifier(Set<T.ID>)
case closure(@Sendable (T) -> Bool)
case and(lhs: Filter, rhs: Filter)
case or(lhs: Filter, rhs: Filter)

func evaluate(on value: T) -> Bool {
switch self {
case let .identifier(ids):
ids.contains(value.id)
case let .closure(closure):
closure(value)
case let .and(lhs, rhs):
lhs.evaluate(on: value) && rhs.evaluate(on: value)
case let .or(lhs, rhs):
lhs.evaluate(on: value) || rhs.evaluate(on: value)
}
}
}

var filter: Filter?
var sortBy: [SortDescriptor<T>] = []

public func perform(on data: any Sequence<T>) -> [T] {
var result: any Sequence<T> = data
if let filter {
result = result.filter { filter.evaluate(on: $0) }
}
return result.sorted(using: sortBy)
}

public static var all: Self { .init() }

public static func identifier(in ids: Set<T.ID>) -> Self {
.init(filter: .identifier(ids))
}

public static func search(_ query: String, minScore: Double = 0.7, transform: @escaping (T) -> String) -> Self {
let term = StringRankedSearch(searchTerm: query)
return .init(filter: .closure { term.score(for: transform($0)) >= minScore })
}
}
24 changes: 15 additions & 9 deletions WordPress/Classes/Utility/DataStore/InMemoryDataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ import Foundation
import Combine

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

/// A publisher for sending and subscribing data changes.
///
/// The publisher emits events when data changes, with identifiers of changed models.
///
/// The publisher does not complete as long as the `InMemoryDataStore` remains alive and valid.
var updates: PassthroughSubject<Set<T.ID>, Never> { get }
}
let updates: PassthroughSubject<Set<T.ID>, Never> = .init()

deinit {
updates.send(completion: .finished)
}

public extension InMemoryDataStore {
func delete(query: Query) async throws {
public func delete(query: DataStoreQuery<T>) async throws {
var updated = Set<T.ID>()
let result = try await list(query: query)
let result = try list(query: query)
result.forEach {
if storage.removeValue(forKey: $0.id) != nil {
updated.insert($0.id)
Expand All @@ -29,7 +31,7 @@ public extension InMemoryDataStore {
}
}

func store(_ data: [T]) async throws {
public func store(_ data: [T]) async throws {
var updated = Set<T.ID>()
data.forEach {
updated.insert($0.id)
Expand All @@ -41,7 +43,11 @@ public extension InMemoryDataStore {
}
}

func listStream(query: Query) -> AsyncStream<Result<[T], Error>> {
public func list(query: DataStoreQuery<T>) throws -> [T] {
query.perform(on: storage.values)
}

public func listStream(query: DataStoreQuery<T>) -> AsyncStream<Result<[T], Error>> {
let stream = AsyncStream<Result<[T], Error>>.makeStream()

let updatingTask = Task { [weak self] in
Expand Down
8 changes: 4 additions & 4 deletions WordPress/WordPressTest/DataStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct InMemoryDataStoreTests {

@Test
func testUpdatesAfterCreation() async {
let store: InMemoryUserDataStore = InMemoryUserDataStore()
let store = InMemoryDataStore<DisplayUser>()
let stream = await store.listStream(query: .all)

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

@Test
func testUpdatesAfterStore() async {
let store: InMemoryUserDataStore = InMemoryUserDataStore()
let store = InMemoryDataStore<DisplayUser>()
let stream = await store.listStream(query: .all)

Task.detached {
Expand All @@ -37,7 +37,7 @@ struct InMemoryDataStoreTests {

@Test
func testUpdatesAfterDelete() async throws {
let store: InMemoryUserDataStore = InMemoryUserDataStore()
let store = InMemoryDataStore<DisplayUser>()
try await store.store([.MockUser])

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

@Test
func testStreamTerminates() async {
var store: InMemoryUserDataStore? = InMemoryUserDataStore()
var store: InMemoryDataStore<DisplayUser>? = .init()
let stream = await store!.listStream(query: .all)

Task.detached {
Expand Down

0 comments on commit 75f15dc

Please sign in to comment.