Skip to content

Commit

Permalink
Add TestStore
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryu0118 committed Sep 18, 2023
1 parent 8bf660a commit 2503c1e
Show file tree
Hide file tree
Showing 20 changed files with 862 additions and 107 deletions.
55 changes: 50 additions & 5 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,57 @@
{
"pins" : [
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
"version" : "1.0.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Ryu0118/swift-case-paths.git",
"location" : "https://github.com/pointfreeco/swift-case-paths.git",
"state" : {
"revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81",
"version" : "1.0.0"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
"version" : "1.0.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "ea631ce892687f5432a833312292b80db238186a",
"version" : "1.0.0"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
"state" : {
"revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01",
"version" : "1.0.0"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies.git",
"state" : {
"branch" : "fix-macro",
"revision" : "49c6dc7c8e9cb56288ee4dd0b14024a896e49b1e"
"revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5",
"version" : "1.0.0"
}
},
{
Expand All @@ -23,8 +68,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865",
"version" : "0.9.0"
"revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
"version" : "1.0.2"
}
}
],
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"),
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", exact: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", exact: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", exact: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand All @@ -31,6 +33,8 @@ let package = Package(
dependencies: [
"SimplexArchitectureMacrosPlugin",
.product(name: "CasePaths", package: "swift-case-paths"),
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
]
),
.macro(
Expand Down
22 changes: 20 additions & 2 deletions Sources/SimplexArchitecture/ActionSendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
/// A protocol for send actions to a store.
public protocol ActionSendable<Reducer> {
associatedtype Reducer: ReducerProtocol<Self>
associatedtype States: StatesProtocol
associatedtype States: StatesProtocol where States.Target == Self

/// The store to which actions will be sent.
var store: Store<Reducer> { get }
Expand All @@ -14,12 +14,30 @@ public extension ActionSendable {
/// Send an action to the store
@discardableResult
func send(_ action: consuming Reducer.Action) -> SendTask {
if store.container == nil {
threadCheck()
return if store.container == nil {
store.sendAction(action, target: self)
} else {
store.sendIfNeeded(action)
}
}

@inline(__always)
func threadCheck() {
#if DEBUG
guard !Thread.isMainThread else {
return
}
runtimeWarning(
"""
"ActionSendable.send" was called on a non-main thread.
The "Store" class is not thread-safe, and so all interactions with an instance of \
"Store" must be done on the main thread.
"""
)
#endif
}
}

public extension ActionSendable where Reducer.Action: Pullbackable {
Expand Down
12 changes: 11 additions & 1 deletion Sources/SimplexArchitecture/Effect/CombineAction.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

public struct CombineAction<Reducer: ReducerProtocol> {
public struct CombineAction<Reducer: ReducerProtocol>: @unchecked Sendable {
@usableFromInline
enum ActionKind {
case viewAction(
action: Reducer.Action
Expand All @@ -10,23 +11,32 @@ public struct CombineAction<Reducer: ReducerProtocol> {
)
}

@usableFromInline
let kind: ActionKind

@usableFromInline
init(kind: ActionKind) {
self.kind = kind
}
}

public extension CombineAction {
@inlinable
static func action(
_ action: Reducer.Action
) -> Self {
.init(kind: .viewAction(action: action))
}

@inlinable
static func action(
_ action: Reducer.ReducerAction
) -> Self {
.init(kind: .reducerAction(action: action))
}
}

extension CombineAction: Equatable where CombineAction.ActionKind: Equatable {}
extension CombineAction.ActionKind: Equatable where Reducer.Action: Equatable, Reducer.ReducerAction: Equatable {}
extension CombineAction: Hashable where CombineAction.ActionKind: Hashable {}
extension CombineAction.ActionKind: Hashable where Reducer.Action: Hashable, Reducer.ReducerAction: Hashable {}
12 changes: 12 additions & 0 deletions Sources/SimplexArchitecture/Effect/EffectTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public struct SideEffect<Reducer: ReducerProtocol>: Sendable {
case concurrentReducerAction([Reducer.ReducerAction])
case serialCombineAction([CombineAction<Reducer>])
case concurrentCombineAction([CombineAction<Reducer>])

@inlinable
var isNone: Bool {
if case .none = self {
return true
}
return false
}
}

let kind: EffectKind
Expand Down Expand Up @@ -62,21 +70,25 @@ public extension SideEffect {
.init(effectKind: .serialAction(actions))
}

@_disfavoredOverload
@inlinable
static func concurrent(_ actions: Reducer.ReducerAction...) -> Self {
.init(effectKind: .concurrentReducerAction(actions))
}

@_disfavoredOverload
@inlinable
static func serial(_ actions: Reducer.ReducerAction...) -> Self {
.init(effectKind: .serialReducerAction(actions))
}

@_disfavoredOverload
@inlinable
static func concurrent(_ actions: CombineAction<Reducer>...) -> Self {
.init(effectKind: .concurrentCombineAction(actions))
}

@_disfavoredOverload
@inlinable
static func serial(_ actions: CombineAction<Reducer>...) -> Self {
.init(effectKind: .serialCombineAction(actions))
Expand Down
40 changes: 40 additions & 0 deletions Sources/SimplexArchitecture/Internal/ActionTransition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

struct ActionTransition<Reducer: ReducerProtocol> {
struct State {
let state: Reducer.Target.States?
let reducerState: Reducer.ReducerState?
}

let previous: Self.State
let next: Self.State
let effect: SideEffect<Reducer>
let effectContext: UUID
let action: CombineAction<Reducer>

init(
previous: Self.State,
next: Self.State,
sideEffect: SideEffect<Reducer>,
effectContext: UUID,
for action: CombineAction<Reducer>
) {
self.previous = previous
self.next = next
effect = sideEffect
self.effectContext = effectContext
self.action = action
}

func toNextStateContainer(from target: Reducer.Target) -> StateContainer<Reducer.Target> {
toStateContainer(from: target, state: next)
}

func toPreviousStateContainer(from target: Reducer.Target) -> StateContainer<Reducer.Target> {
toStateContainer(from: target, state: previous)
}

private func toStateContainer(from target: Reducer.Target, state _: Self.State) -> StateContainer<Reducer.Target> {
.init(target, states: next.state, reducerState: next.reducerState)
}
}
7 changes: 7 additions & 0 deletions Sources/SimplexArchitecture/Internal/Collection+safe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
5 changes: 5 additions & 0 deletions Sources/SimplexArchitecture/Internal/EffectContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

enum EffectContext {
@TaskLocal static var id: UUID?
}
37 changes: 37 additions & 0 deletions Sources/SimplexArchitecture/Internal/Task+withEffectContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

extension Task where Failure == any Error {
@discardableResult
static func withEffectContext(
priority: TaskPriority? = nil,
@_inheritActorContext @_implicitSelfCapture operation: @Sendable @escaping () async throws -> Success
) -> Self {
if let _ = EffectContext.id {
Self(priority: priority, operation: operation)
} else {
Self(priority: priority) {
try await EffectContext.$id.withValue(UUID()) {
try await operation()
}
}
}
}
}

extension Task where Failure == Never {
@discardableResult
static func withEffectContext(
priority: TaskPriority? = nil,
@_inheritActorContext @_implicitSelfCapture operation: @Sendable @escaping () async -> Success
) -> Self {
if let _ = EffectContext.id {
Self(priority: priority, operation: operation)
} else {
Self(priority: priority) {
await EffectContext.$id.withValue(UUID()) {
await operation()
}
}
}
}
}
25 changes: 25 additions & 0 deletions Sources/SimplexArchitecture/Internal/TestOnly.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation
import XCTestDynamicOverlay

@propertyWrapper
struct TestOnly<T> {
private var _value: T

var wrappedValue: T {
_read {
if !_XCTIsTesting {
runtimeWarning("\(Self.self) is accessible only during Unit tests")
}
yield _value
}
set {
if _XCTIsTesting {
_value = newValue
}
}
}

init(wrappedValue: T) {
_value = wrappedValue
}
}
16 changes: 16 additions & 0 deletions Sources/SimplexArchitecture/Reducer/ReducerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,19 @@ public extension ReducerProtocol where ReducerAction == Never {
action _: ReducerAction
) -> SideEffect<Self> {}
}

extension ReducerProtocol {
@inlinable
func reduce(
into state: StateContainer<Target>,
action: CombineAction<Self>
) -> SideEffect<Self> {
switch action.kind {
case .viewAction(let action):
reduce(into: state, action: action)

case .reducerAction(let action):
reduce(into: state, action: action)
}
}
}
2 changes: 2 additions & 0 deletions Sources/SimplexArchitecture/Send.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public struct Send<Reducer: ReducerProtocol>: Sendable {
sendAction(action)
}

@_disfavoredOverload
@discardableResult
@inlinable
func callAsFunction(_ action: Reducer.ReducerAction) -> SendTask {
Expand All @@ -30,6 +31,7 @@ public struct Send<Reducer: ReducerProtocol>: Sendable {
await sendAction(action).wait()
}

@_disfavoredOverload
@MainActor
@inlinable
public func callAsFunction(_ action: Reducer.ReducerAction) async {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SimplexArchitecture/SendTask.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// The type returned from send(_:) that represents the lifecycle of the effect started from sending an action.
public struct SendTask: Sendable {
public struct SendTask: Sendable, Hashable {
static let never = SendTask(task: nil)

@usableFromInline
Expand Down
Loading

0 comments on commit 2503c1e

Please sign in to comment.