Skip to content

Commit

Permalink
Dependency Injection Feature (#116)
Browse files Browse the repository at this point in the history
* added syncronization queue for Injected instances

* updated access synchronization in Injected

* updated Injection macro codegen

* added simple test for injection

* updated macro codegen

* Added docs for 2.1

* minor doc fix

* conditional import to run on CI

* Bumped swift to 5.10

* refactoring

* refactoring

* refactoring

* splitter store injection and dependency inejction

* updated tests

* minor refactoring

* added migration guide link

* minor docs change and deprecation

* minor fix
  • Loading branch information
KazaiMazai authored Sep 18, 2024
1 parent 1a7ee57 commit 8f37de5
Show file tree
Hide file tree
Showing 26 changed files with 520 additions and 83 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:5.10

import PackageDescription
import CompilerPluginSupport
Expand All @@ -18,7 +18,7 @@ let package = Package(
targets: ["Puredux"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0")
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.0")
],
targets: [
.macro(
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ struct AppState {
}
}

// Injected root store
// Inject root store

extension Injected {
@InjectEntry var root = StateStore<AppState, Action>(AppState()) { state, action in
extension SharedStores {
@StoreEntry var root = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/Puredux/DependencyInjection/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Sergey Kazakov on 17/09/2024.
//

import Foundation

public struct Dependencies: Sendable, DependencyContainer {
public init() {

}
}

56 changes: 56 additions & 0 deletions Sources/Puredux/DependencyInjection/Dependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// File.swift
//
//
// Created by Sergey Kazakov on 17/09/2024.
//

import Foundation

@propertyWrapper
public struct Dependency<T> {
private let keyPath: WritableKeyPath<Dependencies, T>

public var wrappedValue: T {
get { Dependencies[keyPath] }
}

public init(_ keyPath: WritableKeyPath<Dependencies, T>) {
self.keyPath = keyPath
}

public static subscript(_ keyPath: WritableKeyPath<Dependencies, T>) -> T {
get {
Dependencies[keyPath]
}
}
}


protocol Service {

}

struct ServiceImp: Service {

}

extension Dependencies {
@DependencyEntry var intValue = 1

@DependencyEntry var uuid = { UUID() }
@DependencyEntry var now = { Date() }

}

class Foo {
@Dependency(\.intValue) var value

var uuidValue = Dependency[\.uuid]

func ffofofo() {
Dependencies[\.now] = { .distantPast }
}
}


42 changes: 42 additions & 0 deletions Sources/Puredux/DependencyInjection/DependencyContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// File.swift
//
//
// Created by Sergey Kazakov on 25/08/2024.
//

import Foundation
import Dispatch

public protocol DependencyContainer: Sendable {
init()
}

/** Provides access to injected dependencies. */
extension DependencyContainer {

/** A static subscript for updating the `currentValue` of `DependencyKey` instances. */
public subscript<Key>(key: Key.Type) -> Key.Value where Key: DependencyKey, Key.Value: Sendable {
get {
DispatchQueue.di.sync { key.currentValue }
}
set {
DispatchQueue.di.async(flags: .barrier) { key.currentValue = newValue }
}
}

/** A static subscript accessor for updating and references dependencies directly. */
public static subscript<T>(_ keyPath: WritableKeyPath<Self, T>) -> T {
get {
Self()[keyPath: keyPath]
}
set {
var instance = Self()
instance[keyPath: keyPath] = newValue
}
}
}

fileprivate extension DispatchQueue {
static let di = DispatchQueue(label: "com.puredux.dependencies", attributes: .concurrent)
}
31 changes: 31 additions & 0 deletions Sources/Puredux/DependencyInjection/DependencyKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// File.swift
//
//
// Created by Sergey Kazakov on 18/09/2024.
//

import Foundation

/**
A protocol that defines a key used for dependency injection.

Types conforming to `DependencyKey` provide a mechanism to inject dependencies by associating a specific type of value (`Value`) .
*/
public protocol DependencyKey {
/** The associated type representing the type of the dependency injection key's value. */
associatedtype Value

/** The default value for the dependency injection key. */
static var currentValue: Self.Value { get set }
}

/**
A protocol that defines a key used for store injection.

Types conforming to `StoreInjectionKey` provide a mechanism to inject .
*/
public protocol StoreInjectionKey: DependencyKey where Value: Store {
/** The associated type representing the type of the dependency injection key's value. */

}
17 changes: 17 additions & 0 deletions Sources/Puredux/DependencyInjection/SharedStores.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// File.swift
//
//
// Created by Sergey Kazakov on 17/09/2024.
//

import Foundation

@available(*, deprecated, message: "use SharedStores instead", renamed: "SharedStores")
public typealias Injected = SharedStores

public struct SharedStores: Sendable, DependencyContainer {
public init() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import SwiftUI
/**
A property wrapper that implements a Dependency Injection (DI) pattern by providing access to an injected instance of `StateStore`.

The `StoreOf` property wrapper is used to access and manage an instance of `StateStore` (or an optional `StateStore`) that is injected into the `Injected` type. This pattern facilitates dependency management by allowing components to retrieve dependencies directly through property wrappers.
The `StoreOf` property wrapper is used to access and manage an instance of `StateStore` (or an optional `StateStore`) that is injected into the `SharedStores` type. This pattern facilitates dependency management by allowing components to retrieve dependencies directly through property wrappers.
- Note: This property wrapper can be initialized with key paths to injected `StateStore` or optional `StateStore` types.
- Parameter T: The type of the `StateStore` or optional `StateStore` being accessed.

Example usage:

```swift

// Use InjectEntry to inject the instance
// Use StoreEntry to inject the store instance

extension Injected {
@InjectEntry var rootState = StateStore<AppRootState, Action>(AppRootState()) { state, action in
extension Stores {
@StoreEntry var rootState = StateStore<AppRootState, Action>(AppRootState()) { state, action in
// Here is a reducer used to mutate the state
}
}
Expand All @@ -42,23 +42,36 @@ Example usage:

@propertyWrapper
public struct StoreOf<T> {
private let keyPath: WritableKeyPath<Injected, T>
private let keyPath: WritableKeyPath<SharedStores, T>

public var wrappedValue: T {
get { Injected[keyPath] }
get { SharedStores[keyPath] }
}

public init<State, Action>(_ keyPath: WritableKeyPath<Injected, T>) where T == StateStore<State, Action> {
public init<State, Action>(_ keyPath: WritableKeyPath<SharedStores, T>) where T == StateStore<State, Action> {
self.keyPath = keyPath
}

public init<State, Action>(_ keyPath: WritableKeyPath<Injected, T>) where T == StateStore<State, Action>? {
public init<State, Action>(_ keyPath: WritableKeyPath<SharedStores, T>) where T == StateStore<State, Action>? {
self.keyPath = keyPath
}

@available(*, deprecated, message: "use StoreOf[keyPath:] instead")
public func store() -> T {
wrappedValue
}

public static subscript<State, Action>(_ keyPath: WritableKeyPath<SharedStores, T>) -> T where T == StateStore<State, Action> {
get {
SharedStores[keyPath]
}
}

public static subscript<State, Action>(_ keyPath: WritableKeyPath<SharedStores, T>) -> T where T == StateStore<State, Action>? {
get {
SharedStores[keyPath]
}
}
}

public extension StoreOf {
Expand Down
117 changes: 117 additions & 0 deletions Sources/Puredux/Documentation.docc/Articles/DependencyInjection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Dependency Injection

Dealing with dependencies in Puredux

## Overview

Dependencies in an application refer to the types and functions that interact with external systems or components beyond your control.

Puredux is designed to offload all "side effects" to the application's "shell," allowing the core logic to remain as pure and isolated as possible.

However, in practice, maintaining a completely pure core can still be challenging. This includes handling things like Date, UUID, Locale, feature flags, and configuration.

In such cases, Dependency Injection becomes a powerful and convenient tool for managing external interactions while preserving the core's integrity.

## Dependencies Injection in Puredux

Puredux splits dependencies into two categories:

- Store Injection
- Dependency Injection

Although both are essentially dependencies, they are handled separately because they serve different purposes, and we want to ensure they remain distinct.

**Store Injection** is used to conveniently obtain store instances in the UI layer of the application.

**Dependency Injection** is used inside the store's reducers to power the application's core logic.

### Stores Injection

Use `@StoreEntry` in the `SharedStores` extension to inject the store instance:


```swift
extension SharedStores {
@StoreEntry var root = StateStore<AppRootState, Action>(....)
}
```

The `@StoreOf` property wrapper can be used to obtain the injected store instance:


```swift
struct MyView: View {
@State @StoreOf(\.root)
var store: StateStore<AppRootState, Action>

var body: some View {
// ...
}
}
```

Alternatively, you can access it directly:

```swift
let store = StoreOf[\.root]
```

If you need to bypass the store's entry point and set the store dependency directly, you can do so via:

```swift

SharedStores[\.root] = StateStore<AppRootState, Action>(...)

```

### Dependency Injection

Use `@DependencyEntry` in the `Dependencies` extension to inject the dependency instance:

```swift
extension Dependencies {
@DependencyEntry var now = { Date() }
}
```

Then it can be used in the app reducer:

```swift
struct AppState {
private var currentTime: Date?

mutating func reduce(_ action: Action) {
switch action {
case let action as UpdateTime:
let now = Dependency[\.now]
currentTime = now()
default:
break
}
}
}

```

If you need to bypass the dependency's entry point and set it directly, you can do so via:


```swift

Dependencies[\.now] = { .distantPast }

```

## Discussion

By implementing a clear separation of concerns between store injection and dependency injection, we gain significant control over their lifecycle and usage:

- Clear Distinction Between Store Injection and Dependency Injection: This separation allows for the swift identification and correction of any misuse or misconfiguration of store hierarchy.

- `StoreEntry` and `DependencyEntry`: These constructs help to clearly define and locate the entry points for both stores and dependencies. This visibility simplifies debugging and maintenance, as developers can quickly trace where and how dependencies and stores are being injected into the application.

- Read-Only Access with `Dependency` and `StoreOf`: These mechanisms provide controlled, read-only access to the dependency injection containers. By restricting modification capabilities, they help maintain the integrity of the state and dependencies throughout the app, reducing the risk of unintended misuse

- Specialization of `StoreOf`: This property wrapper is specifically designed to support only `StateStore` types, which are responsible for owning the state. `@StoreEntry` only supports values of `any Store` type.

- Controlled Access with `Dependencies` and `SharedStores`: These tools offer read and write access to the dependency injection containers, but within a confined scope of the application. This limitation ensures that modifications to dependencies and stores are kept localized, reducing the risk of unintended changes.
4 changes: 2 additions & 2 deletions Sources/Puredux/Documentation.docc/Articles/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ We create a store that integrates actions and the state for the root AppState. T

```swift

extension Injected {
@InjectEntry var root = StateStore<AppState, Action>(AppState()) { state, action in
extension SharedStores {
@StoreEntry var root = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Learn how to upgrade your application to the newest version of Puredux.
## Topics

- <doc:Migrating-to-2.1.x>
- <doc:Migrating-to-2.0.x>
- <doc:Migrating-to-1.9.x>
- <doc:Migrating-to-1.3.x>
Expand Down
Loading

0 comments on commit 8f37de5

Please sign in to comment.