From e9b51ae8c80838a41d472d39a07d5c0f00ca2d32 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 22 Jul 2024 00:11:39 +0200 Subject: [PATCH 1/3] Allow to load multiple modules of the same type --- .../Property/DependencyCollection.swift | 24 ++++ .../Property/DependencyContext.swift | 32 ++--- Sources/Spezi/Module/Module.swift | 4 +- .../ImplicitlyCreatedModulesKey.swift | 17 +++ .../{ => KnowledgeSources}/SpeziAnchor.swift | 0 .../Spezi/KnowledgeSources/SpeziStorage.swift | 16 +++ .../KnowledgeSources/StoredModulesKey.swift | 73 +++++++++++ Sources/Spezi/Spezi/Spezi.swift | 114 +++++++----------- .../Spezi/Utilities/DynamicReference.swift | 35 ++++++ .../DependenciesTests/DependencyTests.swift | 7 ++ 10 files changed, 223 insertions(+), 99 deletions(-) create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift rename Sources/Spezi/Spezi/{ => KnowledgeSources}/SpeziAnchor.swift (100%) create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift create mode 100644 Sources/Spezi/Utilities/DynamicReference.swift diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index cebb9452..db719b57 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -49,6 +49,30 @@ public struct DependencyCollection: DependencyDeclaration { 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:)``. + /// + /// ```swift + /// @resultBuilder + /// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder { + /// public static func buildExpression(_ 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(for type: Dependency.Type = Dependency.self, singleEntry: @escaping @autoclosure (() -> Dependency)) { + self.init(singleEntry: singleEntry) + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = entries.map { $0.dependencyRelation(to: module) } diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 11f9fe0e..971b4592 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -17,23 +17,8 @@ protocol AnyDependencyContext: DependencyDeclaration { class DependencyContext: AnyDependencyContext { - @MainActor - private enum StorageReference: Sendable { - case dependency(Dependency) - case weakDependency(WeaklyStoredModule) - - 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 var injectedDependency: DynamicReference? var isOptional: Bool { @@ -45,7 +30,8 @@ class DependencyContext: AnyDependencyContext { return [] } - guard let module = injectedDependency.value else { + guard let module = injectedDependency.element else { + // TODO: is there any way to re-inject the next one? self.injectedDependency = nil // clear the left over storage return [] } @@ -80,14 +66,14 @@ class DependencyContext: AnyDependencyContext { } if isOptional { - injectedDependency = .weakDependency(WeaklyStoredModule(dependency)) + injectedDependency = .weakElement(dependency) } else { - injectedDependency = .dependency(dependency) + injectedDependency = .element(dependency) } } func uninjectDependencies(notifying spezi: Spezi) { - let dependency = injectedDependency?.value + let dependency = injectedDependency?.element injectedDependency = nil if let dependency { @@ -101,7 +87,7 @@ class DependencyContext: AnyDependencyContext { if let injectedDependency { Task { @MainActor in - guard let dependency = injectedDependency.value else { + guard let dependency = injectedDependency.element else { return } spezi.handleDependencyUninjection(of: dependency) @@ -118,14 +104,14 @@ class DependencyContext: AnyDependencyContext { """ ) } - guard let dependency = injectedDependency.value as? M else { + guard let dependency = injectedDependency.element as? M else { preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M.self)!") } return dependency } func retrieveOptional(dependency: M.Type) -> M? { - guard let dependency = injectedDependency?.value as? M? else { + guard let dependency = injectedDependency?.element as? M? else { preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M?.self)!") } return dependency diff --git a/Sources/Spezi/Module/Module.swift b/Sources/Spezi/Module/Module.swift index 043f5dc7..2dfef5c1 100644 --- a/Sources/Spezi/Module/Module.swift +++ b/Sources/Spezi/Module/Module.swift @@ -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 { +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. diff --git a/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift new file mode 100644 index 00000000..34784563 --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift @@ -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 + typealias Anchor = SpeziAnchor + + static let defaultValue: Value = [] +} diff --git a/Sources/Spezi/Spezi/SpeziAnchor.swift b/Sources/Spezi/Spezi/KnowledgeSources/SpeziAnchor.swift similarity index 100% rename from Sources/Spezi/Spezi/SpeziAnchor.swift rename to Sources/Spezi/Spezi/KnowledgeSources/SpeziAnchor.swift diff --git a/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift b/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift new file mode 100644 index 00000000..a905a6c8 --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift @@ -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 diff --git a/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift new file mode 100644 index 00000000..faf1d6ff --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift @@ -0,0 +1,73 @@ +// +// 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>(in storage: inout Repository) +} + + +struct StoredModulesKey: KnowledgeSource { + typealias Anchor = SpeziAnchor + typealias Value = Self + + var modules: OrderedDictionary> + + var isEmpty: Bool { + modules.isEmpty + } + + init(_ module: DynamicReference, forKey key: ModuleReference) { + modules = [key: module] + } + + func contains(_ key: ModuleReference) -> Bool { + modules[key] != nil + } + + @discardableResult + mutating func updateValue(_ module: DynamicReference, forKey key: ModuleReference) -> DynamicReference? { + modules.updateValue(module, forKey: key) + } + + @discardableResult + mutating func removeValue(forKey key: ModuleReference) -> DynamicReference? { + 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>(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 + } +} diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 05ec20f3..aec8b8aa 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -14,42 +14,6 @@ import SwiftUI import XCTRuntimeAssertions -/// A ``SharedRepository`` implementation that is anchored to ``SpeziAnchor``. -/// -/// This represents the central ``Spezi/Spezi`` storage module. -@_documentation(visibility: internal) -public typealias SpeziStorage = ValueRepository - - -private struct ImplicitlyCreatedModulesKey: DefaultProvidingKnowledgeSource { - typealias Value = Set - typealias Anchor = SpeziAnchor - - static let defaultValue: Value = [] -} - - -private protocol AnyWeaklyStoredModule { - var anyModule: (any Module)? { get } - - @discardableResult - func retrievePurgingIfNil>(in storage: inout Repository) -> (any Module)? -} - - -struct WeaklyStoredModule: KnowledgeSource { - typealias Anchor = SpeziAnchor - typealias Value = Self - - weak var module: M? - - - init(_ module: M) { - self.module = module - } -} - - /// Open-source framework for rapid development of modern, interoperable digital health applications. /// /// Set up the Spezi framework in your `App` instance of your SwiftUI application using the ``SpeziAppDelegate`` and the `@ApplicationDelegateAdaptor` property wrapper. @@ -154,20 +118,28 @@ public final class Spezi: Sendable { ) @_spi(Spezi) public var lifecycleHandler: [LifecycleHandler] { - storage.collect(allOf: LifecycleHandler.self) + modules.compactMap { module in + module as? LifecycleHandler + } } var notificationTokenHandler: [NotificationTokenHandler] { - storage.collect(allOf: NotificationTokenHandler.self) + modules.compactMap { module in + module as? NotificationTokenHandler + } } var notificationHandler: [NotificationHandler] { - storage.collect(allOf: NotificationHandler.self) + modules.compactMap { module in + module as? NotificationHandler + } } var modules: [any Module] { - storage.collect(allOf: (any Module).self) - + storage.collect(allOf: (any AnyWeaklyStoredModule).self).compactMap { $0.retrievePurgingIfNil(in: &storage) } + storage.collect(allOf: (any AnyStoredModules).self) + .reduce(into: []) { partialResult, modules in + partialResult.append(contentsOf: modules.anyModules) + } } @MainActor private var implicitlyCreatedModules: Set { @@ -428,10 +400,11 @@ public final class Spezi: Sendable { /// If you load `n` modules with self-managed storage policy and then all `n` modules will eventually be deallocated, there might be `n` weak references still stored /// till the next module interaction is performed (e.g., new module is loaded or unloaded). private func purgeWeaklyReferenced() { - let elements = storage.collect(allOf: (any AnyWeaklyStoredModule).self) - for reference in elements { - reference.retrievePurgingIfNil(in: &storage) - } + storage + .collect(allOf: (any AnyStoredModules).self) + .forEach { storedModules in + storedModules.removeNilReferences(in: &storage) + } } } @@ -439,48 +412,43 @@ public final class Spezi: Sendable { extension Module { @MainActor fileprivate func storeModule(into spezi: Spezi) { - guard let value = self as? Value else { - spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") - return - } - spezi.storage[Self.self] = value + storeDynamicReference(.element(self), into: spezi) } @MainActor fileprivate func storeWeakly(into spezi: Spezi) { - guard self is Value else { - spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") - return - } + storeDynamicReference(.weakElement(self), into: spezi) + } - spezi.storage[WeaklyStoredModule.self] = WeaklyStoredModule(self) + @MainActor + fileprivate func storeDynamicReference(_ module: DynamicReference, into spezi: Spezi) { + if spezi.storage.contains(StoredModulesKey.self) { + // swiftlint:disable:next force_unwrapping + spezi.storage[StoredModulesKey.self]!.updateValue(module, forKey: self.reference) + } else { + spezi.storage[StoredModulesKey.self] = StoredModulesKey(module, forKey: reference) + } } @MainActor fileprivate func isLoaded(in spezi: Spezi) -> Bool { - spezi.storage[Self.self] != nil - || spezi.storage[WeaklyStoredModule.self]?.retrievePurgingIfNil(in: &spezi.storage) != nil + guard let storedModules = spezi.storage[StoredModulesKey.self] else { + return false + } + return storedModules.contains(reference) } @MainActor fileprivate func clearModule(from spezi: Spezi) { - spezi.storage[Self.self] = nil - spezi.storage[WeaklyStoredModule.self] = nil - } -} - - -extension WeaklyStoredModule: AnyWeaklyStoredModule { - var anyModule: (any Module)? { - module - } - + guard var storedModules = spezi.storage[StoredModulesKey.self] else { + return + } + storedModules.removeValue(forKey: reference) - func retrievePurgingIfNil>(in storage: inout Repository) -> (any Module)? { - guard let module else { - storage[Self.self] = nil - return nil + if storedModules.isEmpty { + spezi.storage[StoredModulesKey.self] = nil + } else { + spezi.storage[StoredModulesKey.self] = storedModules } - return module } } diff --git a/Sources/Spezi/Utilities/DynamicReference.swift b/Sources/Spezi/Utilities/DynamicReference.swift new file mode 100644 index 00000000..582a9244 --- /dev/null +++ b/Sources/Spezi/Utilities/DynamicReference.swift @@ -0,0 +1,35 @@ +// +// 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 +// + + +@MainActor +enum DynamicReference: Sendable { + struct WeaklyStoredElement { + private(set) nonisolated(unsafe) weak var element: Element? + + init(_ element: Element? = nil) { + self.element = element + } + } + + case element(Element) + case weakElement(WeaklyStoredElement) + + static func weakElement(_ element: Element) -> DynamicReference { + .weakElement(WeaklyStoredElement(element)) + } + + nonisolated var element: Element? { + switch self { + case let .element(element): + return element + case let .weakElement(reference): + return reference.element + } + } +} diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 4e575488..cc558c21 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -495,4 +495,11 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let dut4Module = try XCTUnwrap(dut4.testModule3) XCTAssertEqual(dut4Module.state, 4) } + + @MainActor + func testMultipleDependenciesOfSameType() throws { + // TODO: test that it is possible + // TODO: test that @Dependency only ever takes the first! + // TODO: check that @Dependency automatically takes the next one of the first one was unloaded (weak storage!) + } } From 76a163e6a133372f8a5c7fc5045159ae32e172a5 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 22 Jul 2024 12:39:50 +0200 Subject: [PATCH 2/3] Make sure @Dependency property switch to next dependency of same type if one goes out of scope --- .../Dependencies/DependencyManager.swift | 5 +- .../Property/DependencyCollection.swift | 12 ++-- .../Property/DependencyContext.swift | 36 ++++++---- .../Property/DependencyDeclaration.swift | 6 +- .../Property/DependencyPropertyWrapper.swift | 6 +- .../KnowledgeSources/StoredModulesKey.swift | 10 +++ Sources/Spezi/Spezi/Spezi.swift | 10 +++ .../Spezi/Utilities/DynamicReference.swift | 27 +++++--- .../DependenciesTests/DependencyTests.swift | 68 ++++++++++++++++++- 9 files changed, 138 insertions(+), 42 deletions(-) diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 13a5b6ae..18e2664e 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -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(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 } diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index db719b57..c8b03f8e 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -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 } @@ -99,6 +93,12 @@ public struct DependencyCollection: DependencyDeclaration { } } + 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) diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 971b4592..aabd17e1 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -18,6 +18,7 @@ protocol AnyDependencyContext: DependencyDeclaration { class DependencyContext: AnyDependencyContext { let defaultValue: (() -> Dependency)? + private weak var spezi: Spezi? private var injectedDependency: DynamicReference? @@ -25,18 +26,25 @@ class DependencyContext: AnyDependencyContext { defaultValue == nil } - var injectedDependencies: [any Module] { + private var dependency: Dependency? { guard let injectedDependency else { - return [] + return nil } - guard let module = injectedDependency.element else { - // TODO: is there any way to re-inject the next one? - self.injectedDependency = nil // clear the left over storage - return [] + if let module = injectedDependency.element { + return module } - return [module] + // Otherwise, we have a weakly injected module that was de-initialized. + // 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 + } + + // clear the left over storage + self.injectedDependency = nil + return nil } init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) { @@ -72,6 +80,10 @@ class DependencyContext: AnyDependencyContext { } } + func inject(spezi: Spezi) { + self.spezi = spezi + } + func uninjectDependencies(notifying spezi: Spezi) { let dependency = injectedDependency?.element injectedDependency = nil @@ -95,8 +107,8 @@ class DependencyContext: AnyDependencyContext { } } - func retrieve(dependency: M.Type) -> M { - guard let injectedDependency else { + func retrieve(dependency dependencyType: M.Type) -> M { + guard let dependency else { preconditionFailure( """ A `@Dependency` was accessed before the dependency was activated. \ @@ -104,14 +116,14 @@ class DependencyContext: AnyDependencyContext { """ ) } - guard let dependency = injectedDependency.element 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(dependency: M.Type) -> M? { - guard let dependency = injectedDependency?.element 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 diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 131d824d..11c6c461 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -21,9 +21,6 @@ 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) @@ -31,6 +28,9 @@ protocol DependencyDeclaration { @MainActor func inject(from dependencyManager: DependencyManager) + @MainActor + func inject(spezi: Spezi) + /// Remove all dependency injections. @MainActor func uninjectDependencies(notifying spezi: Spezi) diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index c81ae2fe..c68afb08 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -70,6 +70,7 @@ public class _DependencyPropertyWrapper { // swiftlint:disable:this type_ extension _DependencyPropertyWrapper: SpeziPropertyWrapper { func inject(spezi: Spezi) { self.spezi = spezi + dependencies.inject(spezi: spezi) } func clear() { @@ -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) } diff --git a/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift index faf1d6ff..8f175617 100644 --- a/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift +++ b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift @@ -35,6 +35,16 @@ struct StoredModulesKey: KnowledgeSource { modules[key] != nil } + func retrieveFirstAvailable() -> M? { + for (_, value) in modules { + guard let element = value.element else { + continue + } + return element + } + return nil + } + @discardableResult mutating func updateValue(_ module: DynamicReference, forKey key: ModuleReference) -> DynamicReference? { modules.updateValue(module, forKey: key) diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index aec8b8aa..06905844 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -394,6 +394,16 @@ public final class Spezi: Sendable { } } + func retrieveDependencyReplacement(for type: M.Type) -> M? { + guard let storedModules = storage[StoredModulesKey.self] else { + return nil + } + + let replacement = storedModules.retrieveFirstAvailable() + storedModules.removeNilReferences(in: &storage) // if we ask for a replacement, there is opportunity to clean up weak reference objects + return replacement + } + /// Iterates through weakly referenced modules and purges any nil references. /// /// These references are purged lazily. This is generally no problem because the overall overhead will be linear. diff --git a/Sources/Spezi/Utilities/DynamicReference.swift b/Sources/Spezi/Utilities/DynamicReference.swift index 582a9244..3e6e6646 100644 --- a/Sources/Spezi/Utilities/DynamicReference.swift +++ b/Sources/Spezi/Utilities/DynamicReference.swift @@ -9,20 +9,9 @@ @MainActor enum DynamicReference: Sendable { - struct WeaklyStoredElement { - private(set) nonisolated(unsafe) weak var element: Element? - - init(_ element: Element? = nil) { - self.element = element - } - } - case element(Element) case weakElement(WeaklyStoredElement) - static func weakElement(_ element: Element) -> DynamicReference { - .weakElement(WeaklyStoredElement(element)) - } nonisolated var element: Element? { switch self { @@ -32,4 +21,20 @@ enum DynamicReference: Sendable { return reference.element } } + + + static nonisolated func weakElement(_ element: Element) -> DynamicReference { + .weakElement(WeaklyStoredElement(element)) + } +} + + +extension DynamicReference { + struct WeaklyStoredElement { + private(set) nonisolated(unsafe) weak var element: Element? + + init(_ element: Element? = nil) { + self.element = element + } + } } diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index cc558c21..41dc794a 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -126,6 +126,10 @@ private final class TestModule8: Module { init() {} } +private final class SimpleOptionalModuleDependency: Module { + @Dependency var testModule6: TestModule6? +} + final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_length @MainActor @@ -498,8 +502,66 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le @MainActor func testMultipleDependenciesOfSameType() throws { - // TODO: test that it is possible - // TODO: test that @Dependency only ever takes the first! - // TODO: check that @Dependency automatically takes the next one of the first one was unloaded (weak storage!) + let first = TestModule5() + let second = TestModule5() + + let spezi = Spezi(standard: DefaultStandard(), modules: [first, second, TestModule4()]) + + let modules = spezi.modules + func getModule(_ module: M.Type = M.self) throws -> M { + try XCTUnwrap(modules.first(where: { $0 is M }) as? M) + } + + XCTAssertEqual(modules.count, 4) // 3 modules + standard + _ = try getModule(DefaultStandard.self) + + let testModule4 = try getModule(TestModule4.self) + + XCTAssertTrue(testModule4.testModule5 === first) + } + + @MainActor + func testUnloadingWeakDependencyOfSameType() async throws { + let spezi = Spezi(standard: DefaultStandard(), modules: [SimpleOptionalModuleDependency()]) + + let modules = spezi.modules + func getModule(_ module: M.Type = M.self) throws -> M { + try XCTUnwrap(modules.first(where: { $0 is M }) as? M) + } + + XCTAssertEqual(modules.count, 2) + _ = try getModule(DefaultStandard.self) + let module = try getModule(SimpleOptionalModuleDependency.self) + + XCTAssertNil(module.testModule6) + + let dynamicModule6 = TestModule6() + let baseModule6 = TestModule6() + + let scope = { + let weakModule6 = TestModule6() + + spezi.loadModule(weakModule6, ownership: .external) + spezi.loadModule(dynamicModule6) + spezi.loadModule(baseModule6) + + // should contain the first loaded dependency + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === weakModule6) + } + + scope() + + // after externally managed dependency goes out of scope it should automatically switch to next dependency + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === dynamicModule6) + + spezi.unloadModule(dynamicModule6) + + // after manual unload it should take the next available + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === baseModule6) } } + +// swiftlint:disable:this file_length From 485c60c66ea0b339563c405033d7f8200c994bc9 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 22 Jul 2024 13:34:13 +0200 Subject: [PATCH 3/3] Minor docs adjustments --- .../Spezi/Dependencies/Property/DependencyCollection.swift | 4 ++-- .../Dependencies/Property/DependencyCollectionBuilder.swift | 2 +- Sources/Spezi/Spezi.docc/Module/Module.md | 4 ---- Sources/Spezi/Spezi.docc/Standard.md | 4 ---- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index c8b03f8e..2a83d9b2 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -27,7 +27,7 @@ public struct DependencyCollection: DependencyDeclaration { /// ### 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 @@ -51,7 +51,7 @@ public struct DependencyCollection: DependencyDeclaration { /// ### 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:)-6nzui``. /// /// ```swift /// @resultBuilder diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift index f353e50f..55afb6f0 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift @@ -16,7 +16,7 @@ /// static func buildExpression(_ 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 {} diff --git a/Sources/Spezi/Spezi.docc/Module/Module.md b/Sources/Spezi/Spezi.docc/Module/Module.md index 950dcd0c..687332c6 100644 --- a/Sources/Spezi/Spezi.docc/Module/Module.md +++ b/Sources/Spezi/Spezi.docc/Module/Module.md @@ -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. diff --git a/Sources/Spezi/Spezi.docc/Standard.md b/Sources/Spezi/Spezi.docc/Standard.md index 99b637dc..4ee123a6 100644 --- a/Sources/Spezi/Spezi.docc/Standard.md +++ b/Sources/Spezi/Spezi.docc/Standard.md @@ -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``.