Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to load multiple modules of the same type #110

Merged
merged 3 commits into from
Jul 23, 2024
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
5 changes: 3 additions & 2 deletions Sources/Spezi/Dependencies/DependencyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ public class DependencyManager: Sendable {
/// - optional: Flag indicating if it is a optional return.
/// - Returns: Returns the Module instance. Only optional, if `optional` is set to `true` and no Module was found.
func retrieve<M: Module>(module: M.Type = M.self, optional: Bool = false) -> M? {
guard let candidate = initializedModules.first(where: { type(of: $0) == M.self }) ?? existingModules.first(where: { type(of: $0) == M.self }),
let module = candidate as? M else {
guard let candidate = existingModules.first(where: { type(of: $0) == M.self })
?? initializedModules.first(where: { type(of: $0) == M.self }),
let module = candidate as? M else {
precondition(optional, "Could not located dependency of type \(M.self)!")
return nil
}
Expand Down
38 changes: 31 additions & 7 deletions Sources/Spezi/Dependencies/Property/DependencyCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@
public struct DependencyCollection: DependencyDeclaration {
let entries: [AnyDependencyContext]

var injectedDependencies: [any Module] {
entries.reduce(into: []) { result, dependencies in
result.append(contentsOf: dependencies.injectedDependencies)
}
}

init(_ entries: [AnyDependencyContext]) {
self.entries = entries
}
Expand All @@ -33,7 +27,7 @@
/// ### Usage
///
/// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder.
/// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)``.
/// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6ihsh``.
///
/// ```swift
/// @resultBuilder
Expand All @@ -49,6 +43,30 @@
self.init(DependencyContext(for: type, defaultValue: singleEntry))
}

/// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``.
/// - Parameters:
/// - type: The generic type resulting from the passed closure, has to conform to ``Module``.
/// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``.
///
/// ### Usage
///
/// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder.
/// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6nzui``.
///
/// ```swift
/// @resultBuilder
/// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder {
/// public static func buildExpression<T: SomeTypeConstraint>(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection {
/// DependencyCollection(singleEntry: expression)
/// }
/// }
/// ```
///
/// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder.
public init<Dependency: Module>(for type: Dependency.Type = Dependency.self, singleEntry: @escaping @autoclosure (() -> Dependency)) {
self.init(singleEntry: singleEntry)
}

Check warning on line 68 in Sources/Spezi/Dependencies/Property/DependencyCollection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/Property/DependencyCollection.swift#L66-L68

Added lines #L66 - L68 were not covered by tests


func dependencyRelation(to module: DependencyReference) -> DependencyRelation {
let relations = entries.map { $0.dependencyRelation(to: module) }
Expand All @@ -75,6 +93,12 @@
}
}

func inject(spezi: Spezi) {
for entry in entries {
entry.inject(spezi: spezi)
}
}

func uninjectDependencies(notifying spezi: Spezi) {
for entry in entries {
entry.uninjectDependencies(notifying: spezi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/// static func buildExpression<M: Module>(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection
/// ```
///
/// See ``DependencyCollection/init(for:singleEntry:)`` for an example conformance implementation of the ``DependencyCollectionBuilder``.
/// See ``DependencyCollection/init(for:singleEntry:)-6nzui`` for an example conformance implementation of the ``DependencyCollectionBuilder``.
public protocol DependencyCollectionBuilder {}


Expand Down
60 changes: 29 additions & 31 deletions Sources/Spezi/Dependencies/Property/DependencyContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,34 @@


class DependencyContext<Dependency: Module>: AnyDependencyContext {
@MainActor
private enum StorageReference: Sendable {
case dependency(Dependency)
case weakDependency(WeaklyStoredModule<Dependency>)

nonisolated var value: Dependency? {
switch self {
case let .dependency(module):
return module
case let .weakDependency(reference):
return reference.module
}
}
}

let defaultValue: (() -> Dependency)?
private var injectedDependency: StorageReference?
private weak var spezi: Spezi?
private var injectedDependency: DynamicReference<Dependency>?


var isOptional: Bool {
defaultValue == nil
}

var injectedDependencies: [any Module] {
private var dependency: Dependency? {
guard let injectedDependency else {
return []
return nil
}

if let module = injectedDependency.element {
return module
}

guard let module = injectedDependency.value else {
self.injectedDependency = nil // clear the left over storage
return []
// Otherwise, we have a weakly injected module that was de-initialized.
Supereg marked this conversation as resolved.
Show resolved Hide resolved
// See, if there are multiple modules of the same type and inject the "next" one.
if let replacement = spezi?.retrieveDependencyReplacement(for: Dependency.self) {
self.injectedDependency = .weakElement(replacement) // update injected dependency
return replacement
}

return [module]
// clear the left over storage
self.injectedDependency = nil
return nil

Check warning on line 47 in Sources/Spezi/Dependencies/Property/DependencyContext.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/Property/DependencyContext.swift#L45-L47

Added lines #L45 - L47 were not covered by tests
}

init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) {
Expand Down Expand Up @@ -80,14 +74,18 @@
}

if isOptional {
injectedDependency = .weakDependency(WeaklyStoredModule(dependency))
injectedDependency = .weakElement(dependency)
} else {
injectedDependency = .dependency(dependency)
injectedDependency = .element(dependency)
}
}

func inject(spezi: Spezi) {
self.spezi = spezi
}

func uninjectDependencies(notifying spezi: Spezi) {
let dependency = injectedDependency?.value
let dependency = injectedDependency?.element
injectedDependency = nil

if let dependency {
Expand All @@ -101,31 +99,31 @@

if let injectedDependency {
Task { @MainActor in
guard let dependency = injectedDependency.value else {
guard let dependency = injectedDependency.element else {
return
}
spezi.handleDependencyUninjection(of: dependency)
}
}
}

func retrieve<M>(dependency: M.Type) -> M {
guard let injectedDependency else {
func retrieve<M>(dependency dependencyType: M.Type) -> M {
guard let dependency else {
preconditionFailure(
"""
A `@Dependency` was accessed before the dependency was activated. \
Only access dependencies once the module has been configured and the Spezi initialization is complete.
"""
)
}
guard let dependency = injectedDependency.value as? M else {
guard let dependencyM = dependency as? M else {
preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M.self)!")
}
return dependency
return dependencyM
}

func retrieveOptional<M>(dependency: M.Type) -> M? {
guard let dependency = injectedDependency?.value as? M? else {
guard let dependency = self.dependency as? M? else {
preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M?.self)!")
}
return dependency
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ enum DependencyRelation: Hashable {
///
/// This protocol allows to communicate dependency requirements of a ``Module`` to the ``DependencyManager``.
protocol DependencyDeclaration {
/// List of injected dependencies.
var injectedDependencies: [any Module] { get }

/// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`.
@MainActor
func collect(into dependencyManager: DependencyManager)
/// Inject the dependency instance from the ``DependencyManager``. Use `DependencyManager/retrieve(module:)`.
@MainActor
func inject(from dependencyManager: DependencyManager)

@MainActor
func inject(spezi: Spezi)

/// Remove all dependency injections.
@MainActor
func uninjectDependencies(notifying spezi: Spezi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public class _DependencyPropertyWrapper<Value> { // swiftlint:disable:this type_
extension _DependencyPropertyWrapper: SpeziPropertyWrapper {
func inject(spezi: Spezi) {
self.spezi = spezi
dependencies.inject(spezi: spezi)
}

func clear() {
Expand All @@ -82,11 +83,6 @@ extension _DependencyPropertyWrapper: SpeziPropertyWrapper {


extension _DependencyPropertyWrapper: DependencyDeclaration {
var injectedDependencies: [any Module] {
dependencies.injectedDependencies
}


func dependencyRelation(to module: DependencyReference) -> DependencyRelation {
dependencies.dependencyRelation(to: module)
}
Expand Down
4 changes: 1 addition & 3 deletions Sources/Spezi/Module/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
// SPDX-License-Identifier: MIT
//

import SpeziFoundation


// note: detailed documentation is provided as an article extension in the DocC bundle
/// A `Module` defines a software subsystem that can be configured as part of the ``SpeziAppDelegate/configuration``.
public protocol Module: AnyObject, KnowledgeSource<SpeziAnchor> {
public protocol Module: AnyObject {
/// Called on the initialization of the Spezi instance to perform a lightweight configuration of the module.
///
/// It is advised that longer setup tasks are done in an asynchronous task and started during the call of the configure method.
Expand Down
4 changes: 0 additions & 4 deletions Sources/Spezi/Spezi.docc/Module/Module.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ SPDX-License-Identifier: MIT

-->

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Overview

A ``Module``'s initializer can be used to configure its behavior as a subsystem in Spezi-based software.
Expand Down
4 changes: 0 additions & 4 deletions Sources/Spezi/Spezi.docc/Standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ SPDX-License-Identifier: MIT

-->

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

Modules can use the constraint mechanism to enforce a set of requirements to the ``Standard`` used in the Spezi-based software where the module is used.
This mechanism follows a two-step process detailed in the module documentation: ``Module``.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziFoundation


struct ImplicitlyCreatedModulesKey: DefaultProvidingKnowledgeSource {
typealias Value = Set<ModuleReference>
typealias Anchor = SpeziAnchor

static let defaultValue: Value = []
}
16 changes: 16 additions & 0 deletions Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziFoundation


/// A ``SharedRepository`` implementation that is anchored to ``SpeziAnchor``.
///
/// This represents the central ``Spezi/Spezi`` storage module.
@_documentation(visibility: internal)
public typealias SpeziStorage = ValueRepository<SpeziAnchor>
83 changes: 83 additions & 0 deletions Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import OrderedCollections
import SpeziFoundation


protocol AnyStoredModules {
var anyModules: [any Module] { get }

func removeNilReferences<Repository: SharedRepository<SpeziAnchor>>(in storage: inout Repository)
}


struct StoredModulesKey<M: Module>: KnowledgeSource {
typealias Anchor = SpeziAnchor
typealias Value = Self

var modules: OrderedDictionary<ModuleReference, DynamicReference<M>>

var isEmpty: Bool {
modules.isEmpty
}

init(_ module: DynamicReference<M>, forKey key: ModuleReference) {
modules = [key: module]
}

func contains(_ key: ModuleReference) -> Bool {
modules[key] != nil
}

func retrieveFirstAvailable() -> M? {
for (_, value) in modules {
guard let element = value.element else {
continue
}
return element
}
return nil

Check warning on line 45 in Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift#L45

Added line #L45 was not covered by tests
}

@discardableResult
mutating func updateValue(_ module: DynamicReference<M>, forKey key: ModuleReference) -> DynamicReference<M>? {
modules.updateValue(module, forKey: key)
}

@discardableResult
mutating func removeValue(forKey key: ModuleReference) -> DynamicReference<M>? {
modules.removeValue(forKey: key)
}
}


extension StoredModulesKey: AnyStoredModules {
var anyModules: [any Module] {
modules.reduce(into: []) { partialResult, entry in
guard let element = entry.value.element else {
return
}
partialResult.append(element)
}
}

func removeNilReferences<Repository: SharedRepository<SpeziAnchor>>(in storage: inout Repository) {
guard modules.contains(where: { $0.value.element == nil }) else {
return // no weak references
}

var value = self

value.modules.removeAll { _, value in
value.element == nil
}

storage[Self.self] = value
}
}
Loading
Loading