From 8327b7063550008c68edf238d66cbdd01305d500 Mon Sep 17 00:00:00 2001 From: Maxim Bazarov Date: Sun, 31 Dec 2023 12:50:10 +0100 Subject: [PATCH] Public API revamp, provide a way of multiple wrappers be stacked, enables better flexibility --- .../Container/AtomicState_Tests.swift | 0 .../Container/KeyedState_Tests.swift | 0 .../Observability_Tests.swift | 0 .../StateUnderTest.swift | 0 .../AppUnderTest.swift | 0 .../EffectExecution_Tests.swift | 0 Decide-Tests copy/SwiftUI_Tests.swift | 40 +++ Decide-Tests/SwiftUI_Tests.swift | 37 ++- Decide/Binding/Bind.swift | 124 ++++++++ Decide/Decision/Decision.swift | 64 +++++ Decide/Effect/Effect.swift | 36 +++ Decide/Environment/DefaultEnvironment.swift | 45 --- Decide/Environment/Environment.swift | 76 ++--- .../{Persistency.swift => StateRoot.swift} | 14 +- Decide/Environment/SwiftUI.swift | 39 --- Decide/Mutation/Decision.swift | 270 ------------------ Decide/Mutation/DirectMutation.swift | 129 --------- Decide/Mutation/Effect.swift | 91 ------ .../Observability.swift | 35 +-- Decide/Observability/ObservableState.swift | 91 ++++++ Decide/Persistency/Persistency.swift | 35 +++ Decide/RemoteValue/RemoteSync.swift | 36 +++ Decide/State/DefaultInstance.swift | 45 --- Decide/State/ObservableState.swift | 60 ---- Decide/State/Storage.swift | 61 ---- Decide/Wrappers/Instance.swift | 51 ---- DecideTesting/AssertValueAt.swift | 136 --------- .../InlineDecision+Environment.swift | 31 -- .../Mirror+EnvironmentOverride.swift | 50 ++-- DecideTesting/TestingDecision.swift | 30 +- {Decide/Wrappers => IOWrappers}/Atomic.swift | 0 .../Identifiable.swift | 0 32 files changed, 547 insertions(+), 1079 deletions(-) rename {Decide-Tests => Decide-Tests copy}/Container/AtomicState_Tests.swift (100%) rename {Decide-Tests => Decide-Tests copy}/Container/KeyedState_Tests.swift (100%) rename {Decide-Tests => Decide-Tests copy}/Observability_Tests.swift (100%) rename {Decide-Tests => Decide-Tests copy}/StateUnderTest.swift (100%) rename {Decide-Tests => Decide-Tests copy}/Structured State Mutation/AppUnderTest.swift (100%) rename {Decide-Tests => Decide-Tests copy}/Structured State Mutation/EffectExecution_Tests.swift (100%) create mode 100644 Decide-Tests copy/SwiftUI_Tests.swift create mode 100644 Decide/Binding/Bind.swift create mode 100644 Decide/Decision/Decision.swift create mode 100644 Decide/Effect/Effect.swift delete mode 100644 Decide/Environment/DefaultEnvironment.swift rename Decide/Environment/{Persistency.swift => StateRoot.swift} (51%) delete mode 100644 Decide/Environment/SwiftUI.swift delete mode 100644 Decide/Mutation/Decision.swift delete mode 100644 Decide/Mutation/DirectMutation.swift delete mode 100644 Decide/Mutation/Effect.swift rename Decide/{Environment => Observability}/Observability.swift (57%) create mode 100644 Decide/Observability/ObservableState.swift create mode 100644 Decide/Persistency/Persistency.swift create mode 100644 Decide/RemoteValue/RemoteSync.swift delete mode 100644 Decide/State/DefaultInstance.swift delete mode 100644 Decide/State/ObservableState.swift delete mode 100644 Decide/State/Storage.swift delete mode 100644 Decide/Wrappers/Instance.swift delete mode 100644 DecideTesting/AssertValueAt.swift delete mode 100644 DecideTesting/InlineDecision+Environment.swift rename {Decide/Wrappers => IOWrappers}/Atomic.swift (100%) rename Decide/Wrappers/Keyed.swift => IOWrappers/Identifiable.swift (100%) diff --git a/Decide-Tests/Container/AtomicState_Tests.swift b/Decide-Tests copy/Container/AtomicState_Tests.swift similarity index 100% rename from Decide-Tests/Container/AtomicState_Tests.swift rename to Decide-Tests copy/Container/AtomicState_Tests.swift diff --git a/Decide-Tests/Container/KeyedState_Tests.swift b/Decide-Tests copy/Container/KeyedState_Tests.swift similarity index 100% rename from Decide-Tests/Container/KeyedState_Tests.swift rename to Decide-Tests copy/Container/KeyedState_Tests.swift diff --git a/Decide-Tests/Observability_Tests.swift b/Decide-Tests copy/Observability_Tests.swift similarity index 100% rename from Decide-Tests/Observability_Tests.swift rename to Decide-Tests copy/Observability_Tests.swift diff --git a/Decide-Tests/StateUnderTest.swift b/Decide-Tests copy/StateUnderTest.swift similarity index 100% rename from Decide-Tests/StateUnderTest.swift rename to Decide-Tests copy/StateUnderTest.swift diff --git a/Decide-Tests/Structured State Mutation/AppUnderTest.swift b/Decide-Tests copy/Structured State Mutation/AppUnderTest.swift similarity index 100% rename from Decide-Tests/Structured State Mutation/AppUnderTest.swift rename to Decide-Tests copy/Structured State Mutation/AppUnderTest.swift diff --git a/Decide-Tests/Structured State Mutation/EffectExecution_Tests.swift b/Decide-Tests copy/Structured State Mutation/EffectExecution_Tests.swift similarity index 100% rename from Decide-Tests/Structured State Mutation/EffectExecution_Tests.swift rename to Decide-Tests copy/Structured State Mutation/EffectExecution_Tests.swift diff --git a/Decide-Tests copy/SwiftUI_Tests.swift b/Decide-Tests copy/SwiftUI_Tests.swift new file mode 100644 index 0000000..ec7b047 --- /dev/null +++ b/Decide-Tests copy/SwiftUI_Tests.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import SwiftUI +import Decide +import XCTest +import DecideTesting + +@MainActor final class SwiftUI_Tests: XCTestCase { + + final class Storage: KeyedStorage { + @ObservableState var str = "str-default" + @Mutable @ObservableState var strMutable = "strMutable-default" + } + + struct ViewUnderTest: View { + @BindKeyed(\Storage.$strMutable) var strMutable + @ObserveKeyed(\Storage.$str) var str + @ObserveKeyed(\Storage.$strMutable) var strMutableObserved + + var body: some View { + TextField("", text: strMutable[1]) + Text(str[1]) + Text(strMutableObserved[1]) + } + } + +} + diff --git a/Decide-Tests/SwiftUI_Tests.swift b/Decide-Tests/SwiftUI_Tests.swift index ec7b047..cf07a98 100644 --- a/Decide-Tests/SwiftUI_Tests.swift +++ b/Decide-Tests/SwiftUI_Tests.swift @@ -19,20 +19,39 @@ import DecideTesting @MainActor final class SwiftUI_Tests: XCTestCase { - final class Storage: KeyedStorage { - @ObservableState var str = "str-default" - @Mutable @ObservableState var strMutable = "strMutable-default" + final class Storage: StateRoot { + unowned var environment: Decide.SharedEnvironment + init(environment: Decide.SharedEnvironment) { + self.environment = environment + } + + @ObservableValue + @Persistent + var str = "str-default" + + func doTest() { + } + } + + struct UpdateStr: ValueDecision { + var newValue: String + + func mutate(_ env: Decide.DecisionEnvironment) { +// env[\.Storage.$str] = newValue + } } struct ViewUnderTest: View { - @BindKeyed(\Storage.$strMutable) var strMutable - @ObserveKeyed(\Storage.$str) var str - @ObserveKeyed(\Storage.$strMutable) var strMutableObserved + @SwiftUIBind( + \Storage.$str, + mutate: UpdateStr.self + ) var str var body: some View { - TextField("", text: strMutable[1]) - Text(str[1]) - Text(strMutableObserved[1]) + EmptyView() +// TextField("", text: $str) +// Text(str[1]) +// Text(strMutableObserved[1]) } } diff --git a/Decide/Binding/Bind.swift b/Decide/Binding/Bind.swift new file mode 100644 index 0000000..0f81bbc --- /dev/null +++ b/Decide/Binding/Bind.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +final class ChangesPublisher: ObservableObject {} + +#if canImport(SwiftUI) +import SwiftUI + +@propertyWrapper +@MainActor +public struct SwiftUIBind< + Root: StateRoot, + Value, + Mutation: ValueDecision +>: DynamicProperty { + @SwiftUI.Environment(\.sharedEnvironment) var environment + @ObservedObject var publisher = ChangesPublisher() + + public var wrappedValue: Value { + get { + environment + .get(Root.self)[keyPath: statePath] + .getValueSubscribing( + observer: Observer(publisher) { [weak publisher] in + publisher?.objectWillChange.send() + } + ) + } + set { + environment + .get(Root.self)[keyPath: statePath] + .set(value: newValue) + } + } + + let statePath: KeyPath> + let mutate: Mutation.Type + + public init( + _ statePath: KeyPath>, + mutate: Mutation.Type + ) { + self.statePath = statePath + self.mutate = mutate + } +} +#endif + +public protocol ObservingEnvironmentObject: AnyObject { + var environment: SharedEnvironment { get set} + var onChange: () -> Void { get } +} + +/** + (!) Limited support for non SwiftUI objects, + caveat is that each value this object observes will call the `onUpdate`. + it will lead to multiple updates even when there should be one update. + Might cause to many renderings in UIKit views. + TODO: Improve support merging updates in one update, + may be throttling to one per 0.5 sec )(60sec/120framesPerSec) + */ +@propertyWrapper +@MainActor +public final class Bind where Root: StateRoot { + + let statePath: KeyPath> + + var environment = SharedEnvironment.default + + public init( + _ statePath: KeyPath>, + file: StaticString = #fileID, + line: UInt = #line + ) { + self.statePath = statePath + } + + public static subscript( + _enclosingInstance instance: EnclosingObject, + wrapped wrappedKeyPath: KeyPath, + storage storageKeyPath: KeyPath + ) -> Value + where EnclosingObject: ObservingEnvironmentObject + { + get { + let wrapperInstance = instance[keyPath: storageKeyPath] + let root = wrapperInstance.environment.get(Root.self) + let observableValue = root[keyPath: wrapperInstance.statePath] + +#warning(""" +TODO: Squash updates of any values this instance is subscribed to, +to one update to instance. +""") + let observer = Observer(wrapperInstance) { [weak instance] in + instance?.onChange() + } + + return observableValue.getValueSubscribing(observer: observer) + } + set { + let wrapperInstance = instance[keyPath: storageKeyPath] + let root = wrapperInstance.environment.get(Root.self) + let observableValue = root[keyPath: wrapperInstance.statePath] + observableValue.set(value: newValue) + } + } + + @available(*, unavailable, message: "@DefaultBind can only be enclosed by EnvironmentObservingObject.") + public var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } +} diff --git a/Decide/Decision/Decision.swift b/Decide/Decision/Decision.swift new file mode 100644 index 0000000..469e63f --- /dev/null +++ b/Decide/Decision/Decision.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import Foundation + +public typealias EnvironmentMutation = (DecisionEnvironment) -> Void + +/// Encapsulates values updates applied to the ``ApplicationEnvironment`` immediately. +/// Provided with an ``DecisionEnvironment`` to read and write state. +/// Might return an array of ``Effect``, that will be performed asynchronously +/// within the ``ApplicationEnvironment``. +@MainActor public protocol Decision { + func mutate(_ env: DecisionEnvironment) -> Void +} + + +/// Decision that has a `newValue` to use in `mutate`. +@MainActor public protocol ValueDecision: Decision { + associatedtype Value + var newValue: Value { get } +} + +/// A restricted interface of ``ApplicationEnvironment`` provided to ``Decision``. +@MainActor public final class DecisionEnvironment { + + /** + TODO: Implement isolation, creating a new instance of environment, + that reads value form itself or uses a value from the original environment. + + Storing updated keys is a problem tho + May be storing mutations isn't a bad idea + + But so tempting to remove the transaction part. + */ + + unowned var environment: SharedEnvironment + + var effects = [Effect]() + + init(_ environment: SharedEnvironment) { + self.environment = environment + } +} + +extension Decision { + var debugDescription: String { + String(reflecting: self) + } + + var name: String { + String(describing: type(of: self)) + } +} diff --git a/Decide/Effect/Effect.swift b/Decide/Effect/Effect.swift new file mode 100644 index 0000000..8ee8c6e --- /dev/null +++ b/Decide/Effect/Effect.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Encapsulates asynchronous execution of side-effects e.g. network call. +/// Provided with an ``EffectEnvironment`` to read state and make ``Decision``s. +public protocol Effect: Actor { + func perform(in env: EffectEnvironment) async +} + +/// A restricted interface of ``ApplicationEnvironment`` provided to ``Effect``. +public final class EffectEnvironment { +} + +extension Effect { + public var debugDescription: String { + String(reflecting: self) + } + + nonisolated var name: String { + String(describing: type(of: self)) + + " (" + String(describing: self.self) + ")" + } +} diff --git a/Decide/Environment/DefaultEnvironment.swift b/Decide/Environment/DefaultEnvironment.swift deleted file mode 100644 index 1ca5f0d..0000000 --- a/Decide/Environment/DefaultEnvironment.swift +++ /dev/null @@ -1,45 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation -/// Default environment is a way to have a shared ``ApplicationEnvironment`` -/// across the components that neither ``Decision``, ``Effect`` nor `SwiftUI` views -/// e.g. `UIViewController`, some legacy services etc. -/// -/// Check ``EnvironmentObservingObject`` and ``EnvironmentManagedObject`` to learn -/// how to access ``ObservableState``s in legacy context. -/// -/// Usage: -/// ```swift -/// // A reference to ``ApplicationEnvironment``.default -/// @DefaultEnvironment var environment -/// ``` -@MainActor @propertyWrapper public final class DefaultEnvironment { - public var wrappedValue: ApplicationEnvironment = .default - public init() {} -} - -/// An object managed by environment -/// - Instantiated and held by ``ApplicationEnvironment``. -/// - `environment` value is set to the ``ApplicationEnvironment`` it is executed in. -/// -public protocol EnvironmentManagedObject: AnyObject { - @MainActor var environment: ApplicationEnvironment { get set } -} - -@MainActor public protocol EnvironmentObservingObject: AnyObject { - @MainActor var environment: ApplicationEnvironment { get set } - @MainActor func environmentDidUpdate() -} - diff --git a/Decide/Environment/Environment.swift b/Decide/Environment/Environment.swift index a2dbfba..225460c 100644 --- a/Decide/Environment/Environment.swift +++ b/Decide/Environment/Environment.swift @@ -15,52 +15,52 @@ import Foundation import OSLog -/// ApplicationEnvironment stores instances of ``AtomicStorage`` and ``KeyedStorage`` and provides tools for mutations and asynchronous executions of side-effects. -@MainActor public final class ApplicationEnvironment { +let osLogDecideSubsystem = "Decide" - static let _subsystem = "Decide App Environment" - let _decisionLog = Logger( - subsystem: _subsystem, - category: "Decision" - ) - let _effectLog = Logger( - subsystem: _subsystem, - category: "Effect" - ) +/// Shared environment among all components of the system. +/// Unless overridden in component, ``SharedEnvironment/default`` is used. +public final class SharedEnvironment { + typealias Key = ObjectIdentifier + @MainActor private var warehouse: [Key: any StateRoot] = [:] - enum Key: Hashable { - case atomic(ObjectIdentifier) - case keyed(ObjectIdentifier, AnyHashable) + /// Provides the storage of a given type + /// that conforms to ``EnvironmentStateStorage`` + @MainActor func get(_ type: Root.Type) -> Root { + let key = Key(type) + if let value = warehouse[key] { + return unsafeDowncast(value, to: Root.self) + } + + let value = type.init(environment: self) + warehouse[key] = value + return value } +} - static let `default` = ApplicationEnvironment() +//===----------------------------------------------------------------------===// +// MARK: - SwiftUI Support +//===----------------------------------------------------------------------===// - var storage: [Key: Any] = [:] +#if canImport(SwiftUI) +import SwiftUI - func storage(_ key: Key) -> Storage { - if let state = storage[key] as? Storage { - return state - } - let newValue = Storage.init() - storage[key] = newValue - return newValue - } +private struct SharedEnvironment_SwiftUIEnvironmentKey: EnvironmentKey { + static let defaultValue: SharedEnvironment = .default +} - func observableState( - _ keyPath: KeyPath> - ) -> ObservableState { - let state: Storage = storage(Storage.key()) - return state[keyPath: keyPath] - } +public extension EnvironmentValues { - func observableState, Value>( - _ keyPath: KeyPath>, - at id: Identifier - ) -> ObservableState { - let state: Storage = storage(Storage.key(id)) - return state[keyPath: keyPath] + /// Overrides ``Environment`` in the SwiftUI View environment + var sharedEnvironment: SharedEnvironment { + get { self[SharedEnvironment_SwiftUIEnvironmentKey.self] } + set { self[SharedEnvironment_SwiftUIEnvironmentKey.self] = newValue } } - - public init() {} } +public extension View { + /// Overrides ``Environment`` in the view environment` + func sharedEnvironment(_ value: SharedEnvironment) -> some View { + environment(\.sharedEnvironment, value) + } +} +#endif diff --git a/Decide/Environment/Persistency.swift b/Decide/Environment/StateRoot.swift similarity index 51% rename from Decide/Environment/Persistency.swift rename to Decide/Environment/StateRoot.swift index 2242911..d655673 100644 --- a/Decide/Environment/Persistency.swift +++ b/Decide/Environment/StateRoot.swift @@ -12,10 +12,14 @@ // //===----------------------------------------------------------------------===// -import Foundation +/// Describes a "region" of in the environment to store ``ObservableState``. +/// Serves as a root type for the ``ObservableState`` keys, +/// e.g. `\MyState.myValue`. Here `MyState` is a ``StateRoot``. +@MainActor public protocol StateRoot: AnyObject { + var environment: SharedEnvironment { get } + init(environment: SharedEnvironment) +} -@MainActor public class PersistencyStrategy { - func valueDidChange(value: Value) { - fatalError("Not implemented") - } +public extension SharedEnvironment { + static let `default` = SharedEnvironment() } diff --git a/Decide/Environment/SwiftUI.swift b/Decide/Environment/SwiftUI.swift deleted file mode 100644 index e9bb4ee..0000000 --- a/Decide/Environment/SwiftUI.swift +++ /dev/null @@ -1,39 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import SwiftUI - -@MainActor private struct ApplicationEnvironmentKey: EnvironmentKey { - /// Default ``ApplicationEnvironment`` value - public static let defaultValue: ApplicationEnvironment = .default -} - -public extension EnvironmentValues { - - /// Overrides ``ApplicationEnvironment`` in the view environment` - var stateEnvironment: ApplicationEnvironment { - get { self[ApplicationEnvironmentKey.self] } - set { self[ApplicationEnvironmentKey.self] = newValue } - } -} - -public extension View { - - /// Overrides ``ApplicationEnvironment`` in the view environment` - func appEnvironment(_ value: ApplicationEnvironment) -> some View { - environment(\.stateEnvironment, value) - } -} - - diff --git a/Decide/Mutation/Decision.swift b/Decide/Mutation/Decision.swift deleted file mode 100644 index f342fa4..0000000 --- a/Decide/Mutation/Decision.swift +++ /dev/null @@ -1,270 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation - -public typealias EnvironmentMutation = (DecisionEnvironment) -> Void -/// Encapsulates value updates applied to the ``ApplicationEnvironment`` immediately. -/// Provided with an ``DecisionEnvironment`` to read and write state. -/// Might return an array of ``Effect``, that will be performed asynchronously -/// within the ``ApplicationEnvironment``. -@MainActor public protocol Decision { - func mutate(_ env: DecisionEnvironment) -> Void -} - -/// A restricted interface of ``ApplicationEnvironment`` provided to ``Decision``. -@MainActor public final class DecisionEnvironment { - - unowned var environment: ApplicationEnvironment - - var transactions: Set = [] - var effects: [Effect] = [] - - init(_ environment: ApplicationEnvironment) { - self.environment = environment - } - - public subscript( - _ keyPath: KeyPath> - ) -> Value { - get { - environment.observableState(keyPath).wrappedValue - } - set { - transactions.insert( - Transaction(keyPath, newValue: newValue) - ) - } - } - - public subscript, Value>( - _ keyPath: KeyPath>, - at identifier: Identifier - ) -> Value { - get { - environment.observableState(keyPath, at: identifier).wrappedValue - } - set { - transactions.insert( - Transaction(keyPath, newValue: newValue, at: identifier) - ) - } - } - - public func perform(effect: SideEffect) { - effects.append(effect) - } -} - - -/// Incapsulated a container and a new value. -/// Value later is used to apply on environment -/// using `.mutate()` or `.popObservers()`. -final class Transaction: Hashable { - - /// We store the ``ValueContainer`` KeyPath for the transaction identity. - let identity: AnyHashable - let _description: String - - /// called later when changes new value needs to be written in provided environment. - let mutate: (ApplicationEnvironment) -> Void - /// called later when observers of the container in the given environment are needed. - let popObservers: (ApplicationEnvironment) -> Set - - /// Instantiates a transaction of writing a `newValue` at `containerKeyPath`. - /// - Parameters: - /// - containerKeyPath: ``ValueContainer`` KeyPath - /// - newValue: Value to be written. - @MainActor init( - _ keyPath: KeyPath>, - newValue: Value - ) { - // We don't want Transaction to inherit generic of V in ValueContainer, - // so instead of storing the container we pack it into closures that are - // of non-generic types and can be later provided with the environment. - self.mutate = { environment in - environment - .observableState(keyPath) - .wrappedValue = newValue - } - self.popObservers = { environment in - environment - .observableState(keyPath) - .observerStorage - .popObservers() - } - self.identity = keyPath - self._description = String(describing: keyPath) - } - - @MainActor init, Value>( - _ keyPath: KeyPath>, - newValue: Value, - at identifier: Identifier - ) { - self.mutate = { environment in - environment - .observableState(keyPath, at: identifier) - .wrappedValue = newValue - } - self.popObservers = { environment in - environment - .observableState(keyPath, at: identifier) - .observerStorage - .popObservers() - } - self.identity = KeyedIdentity(keyPath: keyPath, id: identifier) - self._description = String(describing: keyPath) - } - - struct KeyedIdentity: Hashable { - let keyPath: AnyHashable - let id: AnyHashable - } - - static func == (lhs: Transaction, rhs: Transaction) -> Bool { - lhs.identity == rhs.identity - } - - func hash(into hasher: inout Hasher) { - hasher.combine(identity) - } -} - -extension Transaction { - var debugDescription: String { - _description - } -} - -extension ApplicationEnvironment { - //===------------------------------------------------------------------===// - // MARK: - Sync - //===------------------------------------------------------------------===// - - /// Synchronously applies state updates from ``Decision``, - /// and then performs all produced ``Effect`` if any produced. - /// **Note:** - /// All effects execution will be postponed till after state updates, - /// and will be executed at once. - public func make(decision: Decision, context: Context) { - // We need to copy the code form `makeAwaiting` here because otherwise - // if we dispatch it from here using `Task`, - // we would interfere the order of state mutations, - // making it possible for decision that made later to be executed before - // the one that was made earlier, which won't be consistent across runs. - // This violates the fundamental idea of structured state management - // single reliable direction of state updates. - let environment = DecisionEnvironment(self) - decision.mutate(environment) - self.apply(transactions: environment.transactions) - _decisionLog.trace(""" - MUT: \(decision.name): - \t states: \(environment.transactions.map{$0.debugDescription}.joined(separator: ", ")) - \t (made at: \(context.debugDescription)) - """) - let _effectsList = environment.effects.map{$0.name}.joined(separator: ", ") - - _decisionLog.trace(""" - EFF: \(decision.name): - \t effects: \(_effectsList) - \t (made at: \(context.debugDescription)) - """) - - // here we only detach effects using unstructured concurrency. - // This must be the only difference with `makeAwaiting` - let effects = environment.effects - Task.detached { - await self.perform(effects: effects, context: context) - } - } - - //===------------------------------------------------------------------===// - // MARK: - Async - //===------------------------------------------------------------------===// - - /// Synchronously applies state updates from ``Decision``, - /// and then awaits all produced ``Effect`` execution to finish - /// if any effects are produced. - /// **Note:** - /// All effects execution will be postponed till after state updates, - /// and will be executed at once. - public func makeAwaiting(decision: Decision, context: Context) async { - let environment = DecisionEnvironment(self) - decision.mutate(environment) - apply(transactions: environment.transactions) - - _decisionLog.trace(""" - \(decision.name): - \t mutates: \(environment.transactions.map{$0.debugDescription}.joined(separator: ", ")) - \t\(context.debugDescription) - """) - let _effectsList = environment.effects.map{$0.name}.joined(separator: ", ") - - _decisionLog.trace(""" - \(decision.name): - \t performs: \(_effectsList) - \t\(context.debugDescription) - """) - await perform(effects: environment.effects, context: context) - } - - //===------------------------------------------------------------------===// - // MARK: - apply Transactions - //===------------------------------------------------------------------===// - - - /// Applies transactions performing the environment mutations - /// and notifies observers of these containers. - @MainActor private func apply(transactions: Set) { - let observers = transactions.reduce(into: Set()) { result, transaction in - result.formUnion(transaction.popObservers(self)) - } - transactions.forEach { transaction in - transaction.mutate(self) - } - observers.forEach{ $0.notify() } - } - - //===------------------------------------------------------------------===// - // MARK: - execute Effects - //===------------------------------------------------------------------===// - - /// context is where execution was performed. - private func perform(effects: [Effect], context: Context) async { - guard effects.count > 0 else { return } - let environment = EffectEnvironment(self) - await withTaskGroup(of: Void.self) { group in - for effect in effects { - _effectLog.trace(""" - started \(effect.name): - \t\(context.debugDescription) - """) - await effect.perform(in: environment) - } - await group.waitForAll() - } - } -} - - -extension Decision { - var debugDescription: String { - String(reflecting: self) - } - - var name: String { - String(describing: type(of: self)) - } -} diff --git a/Decide/Mutation/DirectMutation.swift b/Decide/Mutation/DirectMutation.swift deleted file mode 100644 index 5f5bdd9..0000000 --- a/Decide/Mutation/DirectMutation.swift +++ /dev/null @@ -1,129 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation - -extension ObservableState { - /** - Binds to the provided KeyPath to the ``ObservableState`` or KeyPath + ID. - on get calls the provided `subscribe` closure. - on set calls the sendAll on observers. - */ - @MainActor final class Binding: ObservableObject { - private let stateRef: Reference - private let context: Context - - init(ref: Reference, context: Context) { - self.stateRef = ref - self.context = context - } - - func getValue( - in environment: ApplicationEnvironment, - observer: Observer? - ) -> Value { - let state = stateRef.state(environment) - if let observer { - state.observerStorage.subscribe(observer) - } - return state.wrappedValue - } - - func setValue( - in environment: ApplicationEnvironment, - newValue: Value - ) { - let state = stateRef.state(environment) - state.wrappedValue = newValue - state.observerStorage.sendAll() - } - } -} - -/// Marks observableState as mutable, to use in bindings e.g. ``Bind`` -@propertyWrapper @MainActor public final class Mutable { - private(set) public var wrappedValue: ObservableState - public var projectedValue: Mutable { self } - public init(wrappedValue: ObservableState) { - self.wrappedValue = wrappedValue - } -} - -public extension Observe { - init( - _ keyPath: KeyPath>, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(keyPath.appending(path: \.wrappedValue),file: file, line: line) - } -} - -public extension DefaultObserve { - init( - _ keyPath: KeyPath>, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(keyPath.appending(path: \.wrappedValue),file: file, line: line) - } -} - -public extension ObserveKeyed { - init( - _ keyPath: KeyPath>, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(keyPath.appending(path: \.wrappedValue),file: file, line: line) - } -} - -public extension DefaultObserveKeyed { - init( - _ keyPath: KeyPath>, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.init(keyPath.appending(path: \.wrappedValue),file: file, line: line) - } -} - -public extension DecisionEnvironment { - subscript( - _ keyPath: KeyPath> - ) -> Value { - get { - self[keyPath.appending(path: \.wrappedValue)] - } - set { - self[keyPath.appending(path: \.wrappedValue)] = newValue - } - } - - subscript, Value>( - _ keyPath: KeyPath>, - at identifier: Identifier - ) -> Value { - get { - self[keyPath.appending(path: \.wrappedValue), at: identifier] - } - set { - self[keyPath.appending(path: \.wrappedValue), at: identifier] = newValue - } - } -} - -extension EffectEnvironment {} - diff --git a/Decide/Mutation/Effect.swift b/Decide/Mutation/Effect.swift deleted file mode 100644 index 10dd676..0000000 --- a/Decide/Mutation/Effect.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// Encapsulates asynchronous execution of side-effects e.g. network call. -/// Provided with an ``EffectEnvironment`` to read state and make ``Decision``s. -public protocol Effect: Actor { - func perform(in env: EffectEnvironment) async -} - -/// A restricted interface of ``ApplicationEnvironment`` provided to ``Effect``. -@MainActor public final class EffectEnvironment { - unowned var environment: ApplicationEnvironment - - init(_ environment: ApplicationEnvironment) { - self.environment = environment - } - - public subscript(_ keyPath: KeyPath>) -> Value { - get { environment.observableState(keyPath).wrappedValue } - } - - public subscript( - _ keyPath: KeyPath>, - at identifier: Identifier - ) -> Value - where Identifier: Hashable, Storage: KeyedStorage - { - get { environment.observableState(keyPath, at: identifier).wrappedValue } - } - - public subscript(_ keyPath: KeyPath>) -> Value { - get { - self[keyPath.appending(path: \.wrappedValue)] - } - } - - public subscript( - _ keyPath: KeyPath>, - at identifier: Identifier - ) -> Value - where Identifier: Hashable, Storage: KeyedStorage - { - get { - self[keyPath.appending(path: \.wrappedValue), at: identifier] - } - } - - /// Makes a decision and awaits for all the effects. - public func make( - decision: Decision, - file: StaticString = #file, - line: UInt = #line - ) async { - await environment.makeAwaiting(decision: decision, context: Context(file: file, line: line)) - } - - @MainActor public func instance(_ keyPath: KeyPath>) -> Object { - let object = environment.defaultInstance(at: keyPath).wrappedValue - return object - } - - @MainActor public func instance(_ keyPath: KeyPath>) -> Object { - let object = environment.defaultInstance(at: keyPath).wrappedValue - object.environment = self.environment - return object - } -} - -extension Effect { - public var debugDescription: String { - String(reflecting: self) - } - - nonisolated var name: String { - String(describing: type(of: self)) - + " (" + String(describing: self.self) + ")" - } -} diff --git a/Decide/Environment/Observability.swift b/Decide/Observability/Observability.swift similarity index 57% rename from Decide/Environment/Observability.swift rename to Decide/Observability/Observability.swift index 58dc00c..93ce19f 100644 --- a/Decide/Environment/Observability.swift +++ b/Decide/Observability/Observability.swift @@ -13,45 +13,22 @@ //===----------------------------------------------------------------------===// import Foundation -import Combine - /// Holds a week reference to actual observer, notifies only if object still exist. -final class Observer: Hashable { - final class Notification { - let notify: () -> Void - - init(notify: @escaping () -> Void) { - self.notify = notify - } - } - - private var notification: Notification +public final class Observer: Hashable { + private(set) var notify: () -> Void private var id: ObjectIdentifier - @MainActor init(_ observer: O) where O.ObjectWillChangePublisher == ObservableObjectPublisher{ - self.notification = Notification { [weak observer] in - observer?.objectWillChange.send() - } + @MainActor init(_ observer: O, notify: @escaping () -> Void) { + self.notify = notify self.id = ObjectIdentifier(observer) } - @MainActor init(_ observer: EnvironmentObservingObject) { - self.notification = Notification { [weak observer] in - observer?.environmentDidUpdate() - } - self.id = ObjectIdentifier(observer) - } - - @MainActor func notify() { - notification.notify() - } - - static func == (lhs: Observer, rhs: Observer) -> Bool { + public static func == (lhs: Observer, rhs: Observer) -> Bool { return lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(id) } } diff --git a/Decide/Observability/ObservableState.swift b/Decide/Observability/ObservableState.swift new file mode 100644 index 0000000..be324f8 --- /dev/null +++ b/Decide/Observability/ObservableState.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + + +/** + Contains the reference to the storage of the value. + */ +@MainActor +public protocol ObservableValueWrapper { + associatedtype Value + var storage: ValueStorage { get } +} + + +@MainActor +public final class ValueStorage { + public var initialValue: () -> Value + public var value: Value { + get { + if let value = _value { + return value + } + + let newValue = initialValue() + _value = newValue + return newValue + } + set { + _value = newValue + } + } + + var _value: Value? + init( + initialValue: @escaping () -> Value + ) { + self.initialValue = initialValue + } +} + +/** + A wrapper that wraps the ``ValueStorage``. + **Observability**: guaranties that any change in the value cause a notification to all observers + */ +@propertyWrapper +@MainActor +public final class ObservableValue { + + public var wrappedValue: Value { + valueStorage.value + } + + public var projectedValue: ObservableValue { self } + + var valueStorage: ValueStorage + private var observation = ObserverStorage() + + public init(wrappedValue: @autoclosure @escaping () -> Value) { + self.valueStorage = ValueStorage(initialValue: wrappedValue) + } + + public init(wrappedValue: Wrapper) where Wrapper.Value == Value { + self.valueStorage = wrappedValue.storage + } +} + +extension ObservableValue { + public func getValueSubscribing(observer: Observer) -> Value { + observation.subscribe(observer) + return wrappedValue + } + + public func set(value newValue: Value) { + valueStorage.value = newValue + observation.sendAll() + } +} + + + diff --git a/Decide/Persistency/Persistency.swift b/Decide/Persistency/Persistency.swift new file mode 100644 index 0000000..f09c47a --- /dev/null +++ b/Decide/Persistency/Persistency.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import Foundation + +/** + To implement a persistency of the value. + Right now is just an example to prove that it's possible to wrap a value in it + and still maintain the ``Obse`` + */ +@propertyWrapper +@MainActor +public final class Persistent: ObservableValueWrapper { + public var wrappedValue: Value { storage.value } + public var storage: ValueStorage + + public init(wrappedValue: @autoclosure @escaping () -> Value) { + self.storage = ValueStorage(initialValue: wrappedValue) + } + + public init(wrappedValue: Wrapper) where Wrapper.Value == Value { + self.storage = wrappedValue.storage + } +} diff --git a/Decide/RemoteValue/RemoteSync.swift b/Decide/RemoteValue/RemoteSync.swift new file mode 100644 index 0000000..e60fba3 --- /dev/null +++ b/Decide/RemoteValue/RemoteSync.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Decide package open source project +// +// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package +// open source project authors +// Licensed under MIT +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import Foundation +/** + PLACEHOLDER: + Implementation in later releases. + + Idea: Provide the sync with the remote source e.g. via HTTP request. + Requires the `ObservableAsyncValueWrapper` to accommodate the async nature. + */ +@propertyWrapper +@MainActor +public final class RemoteValue: ObservableValueWrapper { + public var wrappedValue: Value { storage.value } + public var storage: ValueStorage + + public init(wrappedValue: @autoclosure @escaping () -> Value) { + self.storage = ValueStorage(initialValue: wrappedValue) + } + + public init(wrappedValue: Wrapper) where Wrapper.Value == Value { + self.storage = wrappedValue.storage + } +} diff --git a/Decide/State/DefaultInstance.swift b/Decide/State/DefaultInstance.swift deleted file mode 100644 index 9ccd1f0..0000000 --- a/Decide/State/DefaultInstance.swift +++ /dev/null @@ -1,45 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation -/// Managed by ``ApplicationEnvironment`` storage for objects, unlike ``ObservableState`` it doesn't support mutation nor observation. -@propertyWrapper -@MainActor public final class DefaultInstance { - public var wrappedValue: Object { - get { - if let storage { return storage } - let newValue = defaultValue() - storage = newValue - return newValue - } - } - - public var projectedValue: DefaultInstance { - self - } - - public init(wrappedValue: @autoclosure @escaping () -> Object, file: StaticString = #fileID, line: UInt = #line) { - self.defaultValue = wrappedValue - self.file = file.description - self.line = line - } - - // MARK: - Value Storage - private var storage: Object? - private let defaultValue: () -> Object - - // MARK: - Tracing - let file: String - let line: UInt -} diff --git a/Decide/State/ObservableState.swift b/Decide/State/ObservableState.swift deleted file mode 100644 index b604813..0000000 --- a/Decide/State/ObservableState.swift +++ /dev/null @@ -1,60 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation - - -/// Managed by ``ApplicationEnvironment`` storage for values that can be observed and mutated. -@propertyWrapper -@MainActor public final class ObservableState { - let context: Context - var value: Value? - var observerStorage = ObserverStorage() - var persistencyStrategy: PersistencyStrategy? - - let defaultValue: () -> Value - - /// Default Value - public var wrappedValue: Value { - get { - if let value { return value } - let newValue = defaultValue() - value = newValue - return newValue - } - set { - value = newValue - } - } - - public var projectedValue: ObservableState { - self - } - - public init( - wrappedValue: @autoclosure @escaping () -> Value, - file: StaticString = #fileID, - line: UInt = #line - ) { - self.defaultValue = wrappedValue - self.context = Context(file: file, line: line) - } -} - -extension ObservableState { - struct Reference { - let state: (ApplicationEnvironment) -> ObservableState - let debugDescription: String // encoded keypath or keypath + id - } -} diff --git a/Decide/State/Storage.swift b/Decide/State/Storage.swift deleted file mode 100644 index 6d355a6..0000000 --- a/Decide/State/Storage.swift +++ /dev/null @@ -1,61 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Foundation - -@MainActor protocol ObservableStateStorage { - init() -} - -/// AtomicStorage is a managed by ``ApplicationEnvironment`` container for ``ObservableState`` and ``DefaultInstance`` definitions, -/// its only requirement is to provide standalone `init()` so ``ApplicationEnvironment`` can instantiate it when necessary. -/// You should never use instances of ``AtomicStorage`` directly, use ``ObservableState`` or ``DefaultInstance`` instead. -/// -/// **Usage:** -/// ```swift -/// final class TestState: AtomicStorage { -/// // Declaration of a AtomicStorage observableState with a string value -/// // that is "default-value" by default. -/// @ObservableState var name: String = "default-value" -/// -/// // Declaration of the instance of a `NetworkingInterface` protocol -/// // that is `Networking()` by default. -/// @DefaultInstance var networking: NetworkingInterface = Networking() -/// } -/// ``` -@MainActor open class AtomicStorage: ObservableStateStorage { - required public init() {} - - static func key() -> ApplicationEnvironment.Key { - .atomic(ObjectIdentifier(self)) - } -} - -/// KeyedStorage is a collection of ``AtomicStorage`` accessed by `Identifier`. -/// -/// **Usage:** -/// ```swift -/// final class TestKeyedState: KeyedStorage { -/// @ObservableState var name: String = "default-value" -/// } -/// -/// ``` -/// to access the state `Identifier` will have to be provided together with ``ObservableState`` KeyPath. -@MainActor open class KeyedStorage: ObservableStateStorage { - required public init() {} - - static func key(_ identifier: Identifier) -> ApplicationEnvironment.Key { - .keyed(ObjectIdentifier(self), identifier) - } -} diff --git a/Decide/Wrappers/Instance.swift b/Decide/Wrappers/Instance.swift deleted file mode 100644 index 1bcad5d..0000000 --- a/Decide/Wrappers/Instance.swift +++ /dev/null @@ -1,51 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -@propertyWrapper -@MainActor public struct Instance { - - private let instanceKeyPath: KeyPath> - - public init(_ keyPath: KeyPath>) { - self.instanceKeyPath = keyPath - } - - public static subscript( - _enclosingInstance instance: EnclosingObject, - wrapped wrappedKeyPath: KeyPath, - storage storageKeyPath: KeyPath - ) -> Object { - get { - let storage = instance[keyPath: storageKeyPath] - let instanceKeyPath = storage.instanceKeyPath - let environment = instance.environment - return environment.defaultInstance(at: instanceKeyPath).wrappedValue - } - } - - @available(*, unavailable, message: "@Instance must be enclosed in EnvironmentManagedObject.") - public var wrappedValue: Object { - get { fatalError() } - } -} - -extension ApplicationEnvironment { - func defaultInstance( - at keyPath: KeyPath> - ) -> DefaultInstance { - let storage: Storage = storage(Storage.key()) - return storage[keyPath: keyPath] - } -} - diff --git a/DecideTesting/AssertValueAt.swift b/DecideTesting/AssertValueAt.swift deleted file mode 100644 index 2bfc826..0000000 --- a/DecideTesting/AssertValueAt.swift +++ /dev/null @@ -1,136 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import XCTest -@testable import Decide - -public extension ApplicationEnvironment { - //===------------------------------------------------------------------===// - // MARK: - Atomic - //===------------------------------------------------------------------===// - - /// Asserts that value of given container is equal to given value. - func AssertValueIn( - _ valueA: Value, - isEqual valueB: Value, - file: StaticString = #file, - line: UInt = #line - ) { - guard valueA == valueB - else { return XCTFail( - "\(valueA) is not equal to \(valueB)" - , file: file, line: line) - } - } - - func Assert< - Value: Equatable, - Storage: AtomicStorage - >( - _ keyPath: KeyPath>, - _ assertion: (Value) -> Bool, - file: StaticString = #file, - line: UInt = #line - ) { - let containerValue = observableState(keyPath).wrappedValue - guard assertion(containerValue) else { - XCTFail( - "\(containerValue) is not valid" - , file: file, line: line) - return - } - } - /// Asserts that value at given KeyPath is equal to given value. - func AssertValueAt< - Value: Equatable, - Storage: AtomicStorage - >( - _ keyPath: KeyPath>, - isEqual value: Value, - file: StaticString = #file, - line: UInt = #line - ) { - let containerValue = observableState(keyPath).wrappedValue - AssertValueIn(containerValue, isEqual: value, file: file, line: line) - } - - /// Asserts that value at given KeyPath is equal to given value. - func AssertValueAt< - Value: Equatable, - Storage: AtomicStorage - >( - _ keyPath: KeyPath>, - isEqual value: Value, - file: StaticString = #file, - line: UInt = #line - ) { - let containerValue = observableState(keyPath.appending(path: \.wrappedValue)).wrappedValue - AssertValueIn(containerValue, isEqual: value, file: file, line: line) - } - - //===------------------------------------------------------------------===// - // MARK: - Keyed - //===------------------------------------------------------------------===// - - /// Asserts that value of given container and Identifier is equal to given value. - func AssertValueIn< - Value: Equatable, - Identifier: Hashable, - Storage: KeyedStorage - >( - _ valueA: Value, - identifier: Identifier, - isEqual valueB: Value, - file: StaticString = #file, - line: UInt = #line - ) { - guard valueA == valueB - else { return XCTFail( - "\(valueA) at \(identifier) is not equal to \(valueB)" - , file: file, line: line) - } - } - - /// Asserts that value at given KeyPath and Identifier is equal to given value. - func AssertValueAt< - Value: Equatable, - Identifier: Hashable, - Storage: KeyedStorage - >( - _ keyPath: KeyPath>, - at identifier: Identifier, - isEqual value: Value, - file: StaticString = #file, - line: UInt = #line - ) { - let containerValue = observableState(keyPath, at: identifier).wrappedValue - AssertValueIn(containerValue, identifier: identifier, isEqual: value, file: file, line: line) - } - - func AssertValueAt< - Value: Equatable, - Identifier: Hashable, - Storage: KeyedStorage - >( - _ keyPath: KeyPath>, - at identifier: Identifier, - isEqual value: Value, - file: StaticString = #file, - line: UInt = #line - ) { - let containerValue = observableState(keyPath.appending(path: \.wrappedValue), at: identifier).wrappedValue - AssertValueIn(containerValue, identifier: identifier, isEqual: value, file: file, line: line) - } -} - diff --git a/DecideTesting/InlineDecision+Environment.swift b/DecideTesting/InlineDecision+Environment.swift deleted file mode 100644 index 2c33923..0000000 --- a/DecideTesting/InlineDecision+Environment.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Decide package open source project -// -// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package -// open source project authors -// Licensed under MIT -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import Decide - -public extension ApplicationEnvironment { - - private struct DecideTestingMutation: Decision { - func mutate(_ env: Decide.DecisionEnvironment) { - mutation(env) - } - - var mutation: EnvironmentMutation - } - - func makeDecision(_ mutation: @escaping EnvironmentMutation, file: StaticString = #file, line: UInt = #line) { - let decision = DecideTestingMutation(mutation: mutation) - self.make(decision: decision, context: Context(file: file, line: line)) - } -} diff --git a/DecideTesting/Mirror+EnvironmentOverride.swift b/DecideTesting/Mirror+EnvironmentOverride.swift index 185ba99..de8525d 100644 --- a/DecideTesting/Mirror+EnvironmentOverride.swift +++ b/DecideTesting/Mirror+EnvironmentOverride.swift @@ -14,28 +14,28 @@ import Decide -@MainActor public func WithEnvironment( - _ environment: ApplicationEnvironment, - object: Object -) -> Object { - Mirror(reflecting: object).replaceEnvironment(with: environment) - return object -} - -private extension Mirror { - @MainActor func replaceEnvironment(with newEnvironment: ApplicationEnvironment) { - for var child in children { - replaceEnvironment(on: &child, with: newEnvironment) - } - } - - @MainActor func replaceEnvironment(on child: inout Mirror.Child, with newEnvironment: ApplicationEnvironment) { - if let object = child.value as? DefaultEnvironment { - object.wrappedValue = newEnvironment - return - } - - let mirror = Mirror(reflecting: child.value) - mirror.replaceEnvironment(with: newEnvironment) - } -} +//@MainActor public func WithEnvironment( +// _ environment: ApplicationEnvironment, +// object: Object +//) -> Object { +// Mirror(reflecting: object).replaceEnvironment(with: environment) +// return object +//} +// +//private extension Mirror { +// @MainActor func replaceEnvironment(with newEnvironment: ApplicationEnvironment) { +// for var child in children { +// replaceEnvironment(on: &child, with: newEnvironment) +// } +// } +// +// @MainActor func replaceEnvironment(on child: inout Mirror.Child, with newEnvironment: ApplicationEnvironment) { +// if let object = child.value as? SharedEnvironment { +// object.wrappedValue = newEnvironment +// return +// } +// +// let mirror = Mirror(reflecting: child.value) +// mirror.replaceEnvironment(with: newEnvironment) +// } +//} diff --git a/DecideTesting/TestingDecision.swift b/DecideTesting/TestingDecision.swift index 82a971f..296bf5d 100644 --- a/DecideTesting/TestingDecision.swift +++ b/DecideTesting/TestingDecision.swift @@ -14,18 +14,18 @@ import Foundation import Decide - -public extension ApplicationEnvironment { - - private struct TestingDecision: Decision { - func mutate(_ env: Decide.DecisionEnvironment) { - mutation(env) - } - - let mutation: EnvironmentMutation - } - - func decision(_ mutate: @escaping EnvironmentMutation, file: StaticString = #file, line: UInt = #line) { - self.make(decision: TestingDecision(mutation: mutate), context: Context(file: file, line: line)) - } -} +// +//public extension ApplicationEnvironment { +// +// private struct TestingDecision: Decision { +// func mutate(_ env: Decide.DecisionEnvironment) { +// mutation(env) +// } +// +// let mutation: EnvironmentMutation +// } +// +// func decision(_ mutate: @escaping EnvironmentMutation, file: StaticString = #file, line: UInt = #line) { +// self.make(decision: TestingDecision(mutation: mutate), context: Context(file: file, line: line)) +// } +//} diff --git a/Decide/Wrappers/Atomic.swift b/IOWrappers/Atomic.swift similarity index 100% rename from Decide/Wrappers/Atomic.swift rename to IOWrappers/Atomic.swift diff --git a/Decide/Wrappers/Keyed.swift b/IOWrappers/Identifiable.swift similarity index 100% rename from Decide/Wrappers/Keyed.swift rename to IOWrappers/Identifiable.swift