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
46 changes: 46 additions & 0 deletions Decide-Tests/KeyedStateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// 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 KeyedStateTests: XCTestCase {

let propertyUnderTest = \KeyedStateUnderTest.$name

func test_DefaultValue_whenDidNotInitialize() throws {
let sut = StateEnvironment()

sut.Assert(propertyUnderTest,
at: UUID(),
isEqual: KeyedStateUnderTest.defaultSUTName)
}

func test_WriteAndReadState_WithSameID() throws {
let sut = StateEnvironment()
let identifier = UUID()
let value = "my-test-value"

sut.set(value, with: identifier, at: propertyUnderTest)
sut.Assert(propertyUnderTest,
at: identifier,
isEqual: value)

sut.Assert(propertyUnderTest,
at: UUID(),
isEqual: KeyedStateUnderTest.defaultSUTName)
}
}
9 changes: 9 additions & 0 deletions Decide-Tests/StateUnderTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@
//===----------------------------------------------------------------------===//

import Decide
import Foundation

final class StateUnderTest: AtomicState {

static let defaultSUTName = "default-sut-name"

@Property var name: String = defaultSUTName
}

final class KeyedStateUnderTest: KeyedState<UUID> {

static let defaultSUTName = "default-sut-name"

@Property var name: String = defaultSUTName

}
16 changes: 14 additions & 2 deletions Decide/Environment/AtomicState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
//
//===----------------------------------------------------------------------===//

@MainActor protocol State: AnyObject {
init()
}

/// AtomicState is a managed by ``Environment`` container for ``Property`` and ``DefaultInstance`` definitions,
/// its only requirement is to provide standalone `init()` so ``Environment`` can instantiate it when necessary.
/// You should never use instances of ``AtomicState`` directly, use ``Property`` or ``DefaultInstance`` instead.
Expand All @@ -28,6 +32,14 @@
/// @DefaultInstance var networking: NetworkingInterface = Networking()
/// }
/// ```
@MainActor public protocol AtomicState: AnyObject {
init()
@MainActor open class AtomicState: State {
required public init() {

}
}

@MainActor open class KeyedState<Identifier: Hashable>: State {
required public init() {

}
}
29 changes: 29 additions & 0 deletions Decide/Environment/StateEnvironment+AtomicState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// 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 StateEnvironment {
subscript<S: AtomicState>(_ stateType: S.Type) -> S {
let key = Key.atomic(ObjectIdentifier(stateType))
if let state = storage[key] as? S { return state }
let newValue = S.init()
storage[key] = newValue
return newValue
}

func getProperty<S: AtomicState, Value>(_ propertyKeyPath: KeyPath<S, Property<Value>>) -> Property<Value> {
self[S.self][keyPath: propertyKeyPath]
}
}
32 changes: 32 additions & 0 deletions Decide/Environment/StateEnvironment+KeyedState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
//
// 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 StateEnvironment {
subscript<I:Hashable, S: KeyedState<I>>(_ stateType: S.Type, _ identifier: I) -> S {
let key = Key.keyed(ObjectIdentifier(stateType), identifier)
if let state = storage[key] as? S { return state }
let newValue = S.init()
storage[key] = newValue
return newValue
}

func getProperty<I:Hashable, S: KeyedState<I>, Value>(
_ propertyKeyPath: KeyPath<S, Property<Value>>,
at identifier: I
) -> Property<Value> {
self[S.self, identifier][keyPath: propertyKeyPath]
}
}
20 changes: 5 additions & 15 deletions Decide/Environment/StateEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,14 @@
import Foundation

@MainActor public final class StateEnvironment {
typealias StateKey = ObjectIdentifier
enum Key: Hashable {
case atomic(ObjectIdentifier)
case keyed(ObjectIdentifier, AnyHashable)
}

static let `default` = StateEnvironment()

var storage: [StateKey: Any] = [:]

subscript<S: AtomicState>(_ stateType: S.Type) -> S {
let stateKey = StateKey(stateType)
if let state = storage[stateKey] as? S { return state }
let newValue = S.init()
storage[stateKey] = newValue
return newValue
}

func getProperty<S: AtomicState, Value>(_ propertyKeyPath: KeyPath<S, Property<Value>>) -> Property<Value> {
self[S.self][keyPath: propertyKeyPath]
}
var storage: [Key: Any] = [:]

public init() {}
}

23 changes: 23 additions & 0 deletions DecideTesting/Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ public extension StateEnvironment {
let property = getProperty(propertyKeyPath)
property.wrappedValue = value
}

func set<V, I: Hashable, S: KeyedState<I>>(
_ value: V,
with identifier: I,
at propertyKeyPath: KeyPath<S, Property<V>>
) {
let property = getProperty(propertyKeyPath, at: identifier)
property.wrappedValue = value
}
}


Expand All @@ -87,4 +96,18 @@ public extension StateEnvironment {
return XCTFail("\(String(describing: stateValue)) is not equal \(String(describing: value))", file: file, line: line)
}
}

func Assert<V: Equatable, I: Hashable, S: KeyedState<I>>(
_ propertyKeyPath: KeyPath<S, Property<V>>,
at identifier: I,
isEqual value: V,
file: StaticString = #file,
line: UInt = #line
) {
let stateValue = getProperty(propertyKeyPath, at: identifier).wrappedValue
guard stateValue == value
else {
return XCTFail("\(String(describing: stateValue)) is not equal \(String(describing: value))", file: file, line: line)
}
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- [ ] **Observable State**
- [x] Atomic State
- [ ] Keyed State
- [x] Implementation and use in Environment
- [ ] Binding
- [ ] Computed/Selector
- [ ] Keyed Computed/Selector
- [ ] **State Access**
Expand Down