Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions Decide-Tests/Structured State Mutation/AppUnderTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the DecItem.IDe package open source project
//
// Copyright (c) 2020-2023 Maxim Bazarov and the DecItem.IDe package
// open source project authors
// Licensed under MIT
//
// See LICENSE.txt for license information
//
// SPDX-License-Item.IDentifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation
import Decide



/// Application That uses all available APIs of Decide.
/// Must provide 100% coverage of functionality Decide ships.
final class AppUnderTest: AtomicState {

//===------------------------------------------------------------------===//
// MARK: - Dependency Injection
//===------------------------------------------------------------------===//

@DefaultInstance var networking: Networking = URLSession.shared

//===------------------------------------------------------------------===//
// MARK: - Feature Flags
//===------------------------------------------------------------------===//

/// isEnabled["feature-flag-key"]
final class FeatureFlag: KeyedState<String> {
@Property var isEnabled: Bool = false
}

//===------------------------------------------------------------------===//
// MARK: - Item List
//===------------------------------------------------------------------===//
@Property var selectedItemID: Item.ID?
@Property var itemList: [Item.ID] = []

/// Item properties
final class Item: KeyedState<Item.ID> {
typealias ID = UUID

@Property var propString = "propString"
@Mutable @Property var mutpropString = "mutpropString"
@Property var isAvailable: Bool = true
}

//===------------------------------------------------------------------===//
// MARK: - Item Editor
//===------------------------------------------------------------------===//
/// Editor for the item:
/// - Add item
/// - Edit curently selected item
/// - Delete item
///
final class Editor: AtomicState {

/// An ID of the item that is currently edited.
@Property var itemID: Item.ID?


/// Decided to add and select a new item.
struct MustAddAndSelectNewItem: Decision {
func mutate(environment: DecisionEnvironment) {
let newID = Item.ID()
environment[\AppUnderTest.$itemList].append(newID)
environment[\AppUnderTest.$selectedItemID] = newID
}
}

struct MustFetchList: Decision {
func mutate(environment: DecisionEnvironment) {
environment.perform(effect: FetchListOfItems())
}
}

/// Fetch the list of items from the server and update `AppUndertest/itemList`
actor FetchListOfItems: Effect {
func perform(in env: EffectEnvironment) async {
let net = await env.instance(
\AppUnderTest.$networking
)
let _ = await env[
\AppUnderTest.Item.$propString, at: Item.ID()
]
let _: String? = try? await net.fetch(
URLRequest(url: URL(string: "hellofresh.com")!)
)
await env.make(decision: MustUpdateItemList())
}

struct MustUpdateItemList: Decision {
func mutate(environment: DecisionEnvironment) {
environment[\AppUnderTest.$itemList] = [.init()]
}
}
}
}
}

struct Response: Codable {}

protocol Networking {
func fetch<T: Codable>(
_ request: URLRequest,
decode: (Data) async throws -> T
) async throws -> T

func fetch<T: Codable>(
_ request: URLRequest
) async throws -> T
}

extension URLSession: Networking {
func fetch<T>(_ request: URLRequest) async throws -> T where T : Decodable, T : Encodable {
try await fetch(request, decode: { try JSONDecoder().decode(T.self, from: $0) })
}

func fetch<T: Codable>(
_ request: URLRequest,
decode: (Data) async throws -> T
) async throws -> T {
let (data, _) = try await self.data(for: request)
return try await decode(data)
}
}


struct Request {

}
44 changes: 44 additions & 0 deletions Decide-Tests/Structured State Mutation/EffectExecution_Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// 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
import DecideTesting
@testable import Decide


@MainActor final class StateManagementTests: XCTestCase {

let propertyUnderTest = \StateUnderTest.$name



struct MustupdateState: Decision {
let id: UUID

func mutate(environment: Decide.DecisionEnvironment) {
environment[\AppUnderTest.$selectedItemID] = id
environment.perform(effect: AppUnderTest.Editor.FetchListOfItems())
}
}

func test_MakeDecision() async throws {
let sut = ApplicationEnvironment()

await sut.makeAwaiting(decision: AppUnderTest.Editor.MustFetchList())

let items = sut.getValue(\AppUnderTest.$itemList)
XCTAssert(items.count == 1)
}
}

60 changes: 60 additions & 0 deletions Decide/Structured State Mutation/Decision.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
//
// 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 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(environment: DecisionEnvironment)
}

/// A restricted interface of ``ApplicationEnvironment`` provided to ``Decision``.
@MainActor public final class DecisionEnvironment {

unowned var environment: ApplicationEnvironment

var transactions: Set<Transaction> = []
var effects: [Effect] = []

init(_ environment: ApplicationEnvironment) {
self.environment = environment
}

public subscript<S: AtomicState, V>(_ propertyKeyPath: KeyPath<S, Property<V>>) -> V {
get {
environment.getValue(propertyKeyPath)
}
set {
setValue(propertyKeyPath, newValue)
}
}

func setValue<S: AtomicState, V>(
_ keyPath: KeyPath<S, Property<V>>,
_ newValue: V
) {
transactions.insert(
Transaction(keyPath, newValue: newValue)
)
}

public func perform<E: Effect>(effect: E) {
effects.append(effect)
}
}

76 changes: 76 additions & 0 deletions Decide/Structured State Mutation/Effect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//===----------------------------------------------------------------------===//
//
// 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<S: AtomicState, V>(_ propertyKeyPath: KeyPath<S, Property<V>>) -> V {
get { environment.getValue(propertyKeyPath) }
}

public subscript<I, S, V>(
_ propertyKeyPath: KeyPath<S, Property<V>>,
at identifier: I
) -> V
where I: Hashable, S: KeyedState<I>
{
get { environment.getValue(propertyKeyPath, at: identifier) }
}

public subscript<S: AtomicState, V>(_ propertyKeyPath: KeyPath<S, Mutable<V>>) -> V {
get {
environment.getValue(propertyKeyPath.appending(path: \.wrappedValue))
}
}

public subscript<I, S, V>(
_ propertyKeyPath: KeyPath<S, Mutable<V>>,
at identifier: I
) -> V
where I: Hashable, S: KeyedState<I>
{
get {
environment.getValue(propertyKeyPath.appending(path: \.wrappedValue), at: identifier)
}
}

/// Makes a decision and awaits for all the effects.
public func make(decision: Decision) async {
await environment.makeAwaiting(decision: decision)
}

@MainActor public func instance<S: AtomicState, O>(_ keyPath: KeyPath<S, DefaultInstance<O>>) -> O {
let obj = environment.defaultInstance(at: keyPath).wrappedValue
return obj
}

@MainActor public func instance<S: AtomicState, O: EnvironmentManagedObject>(_ keyPath: KeyPath<S, DefaultInstance<O>>) -> O {
let obj = environment.defaultInstance(at: keyPath).wrappedValue
obj.environment = self.environment
return obj
}
}
Loading