From 5eed5e7ca09dd9d8d96607693459ed0019df8d68 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Tue, 4 Jun 2024 16:57:08 +0200 Subject: [PATCH] Finish up implementation --- .../Dependencies/DependencyManager.swift | 32 +++----- .../Spezi/Dependencies/ModuleReference.swift | 65 +--------------- .../Property/DependencyContext.swift | 1 - Sources/Spezi/Spezi/Spezi.swift | 74 ++++++++++++++----- Sources/Spezi/Spezi/View+Spezi.swift | 1 - .../DependenciesTests/DependencyTests.swift | 3 + .../ModuleTests/ModuleBuilderTests.swift | 16 +++- 7 files changed, 83 insertions(+), 109 deletions(-) diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 250c3e1f..94a71057 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -23,10 +23,10 @@ public class DependencyManager { /// A List of `ModuleReference`s that where implicitly created (e.g., due to another module requesting it as a Dependency and /// conforming to ``DefaultInitializable``). /// This list is important to keep for the unload mechanism. - private(set) var implicitlyCreatedModules: ModuleReferences = [] + private(set) var implicitlyCreatedModules: Set = [] /// Collection of all modules with dependencies that are not yet processed. - private var modulesWithDependencies: [any Module] // TODO: different name? + private var modulesWithDependencies: [any Module] /// Recursive search stack to keep track of potential circular dependencies. private var searchStack: [any Module] = [] @@ -51,7 +51,7 @@ public class DependencyManager { /// After calling `resolve` you can safely access `initializedModules`. func resolve() { // Start the dependency resolution on the first module. - if let nextModule = modulesWithDependencies.first { + while let nextModule = modulesWithDependencies.first { push(nextModule) } @@ -74,7 +74,7 @@ public class DependencyManager { for dependency in module.dependencyDeclarations { dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)` } - resolvedAllDependencies(module) + finishSearch(module) } /// Communicate a requirement to a `DependencyManager` @@ -99,7 +99,7 @@ public class DependencyManager { let newModule = defaultValue() - implicitlyCreatedModules.append(ModuleReference(newModule)) + implicitlyCreatedModules.insert(ModuleReference(newModule)) guard !newModule.dependencyDeclarations.isEmpty else { initializedModules.append(newModule) @@ -150,33 +150,25 @@ public class DependencyManager { return module } - - private func resolvedAllDependencies(_ dependingModule: any Module) { + + private func finishSearch(_ dependingModule: any Module) { guard !searchStack.isEmpty else { preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack is empty.") } let module = searchStack.removeLast() - + guard module === dependingModule else { preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack element was not the one we are resolving for.") } - - + + let dependingModulesCount = modulesWithDependencies.count modulesWithDependencies.removeAll(where: { $0 === dependingModule }) precondition( dependingModulesCount - 1 == modulesWithDependencies.count, - "Unexpected reduction of modules. Ensure that all your modules conform to the same `Standard`" // TODO: update message! + "Unexpected reduction of modules." ) - - initializedModules.append(dependingModule) - // TODO: move all above out into a separate method? (cleanup like?) - - // TODO: why is this a recursive search? just make it iterative? - // Call the dependency resolution mechanism on the next element in the `dependingModules` if we are not in a recursive search. - if searchStack.isEmpty, let nextModule = modulesWithDependencies.first { - push(nextModule) - } + initializedModules.append(dependingModule) } } diff --git a/Sources/Spezi/Dependencies/ModuleReference.swift b/Sources/Spezi/Dependencies/ModuleReference.swift index c20ce895..8816ebd3 100644 --- a/Sources/Spezi/Dependencies/ModuleReference.swift +++ b/Sources/Spezi/Dependencies/ModuleReference.swift @@ -6,74 +6,11 @@ // SPDX-License-Identifier: MIT // -import SpeziFoundation - -struct ModuleReferences: DefaultProvidingKnowledgeSource { - static let defaultValue = ModuleReferences() - - typealias Anchor = SpeziAnchor - - private var references: [ModuleReference] - - init(_ references: [ModuleReference]) { - self.references = references - } -} - - -struct ModuleReference: Hashable { // TODO: move +struct ModuleReference: Hashable { private let id: ObjectIdentifier init(_ module: any Module) { self.id = ObjectIdentifier(module) } } - - -extension ModuleReferences: Collection { - typealias Index = Array.Index - - var startIndex: Index { - references.startIndex - } - - var endIndex: Index { - references.endIndex - } - - func index(after index: Index) -> Index { - references.index(after: index) - } - - subscript(position: Index) -> ModuleReference { - references[position] - } -} - - -extension ModuleReferences: RangeReplaceableCollection { - typealias SubSequence = Self - - - init() { - self.init([]) - } - - - mutating func replaceSubrange(_ subrange: Range, with newElements: C) where Element == C.Element { - references.replaceSubrange(subrange, with: newElements) - } - - subscript(bounds: Range) -> ModuleReferences { - let elements = references[bounds] - return ModuleReferences(elements) - } -} - - -extension ModuleReferences: ExpressibleByArrayLiteral { - init(arrayLiteral elements: ModuleReference...) { - self.init(elements) - } -} diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index b794ae4d..b5218236 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -52,7 +52,6 @@ class DependencyContext: AnyDependencyContext { } func inject(from dependencyManager: DependencyManager) { - // TODO: precondition(injectedDependency == nil, "Dependency of type \(Dependency.self) is already injected!") injectedDependency = dependencyManager.retrieve(optional: isOptional) } diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 066817c8..e096640e 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -19,6 +19,15 @@ import XCTRuntimeAssertions @_documentation(visibility: internal) public typealias SpeziStorage = HeapRepository + +private struct ImplicitlyCreatedModulesKey: DefaultProvidingKnowledgeSource { + typealias Value = Set + typealias Anchor = SpeziAnchor + + static let defaultValue: Value = [] +} + + /// 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. @@ -111,8 +120,28 @@ public class Spezi { storage.collect(allOf: (any Module).self) } + private var implicitlyCreatedModules: Set { + get { + storage[ImplicitlyCreatedModulesKey.self] + } + set { + storage[ImplicitlyCreatedModulesKey.self] = newValue + } + } + /// Access the global Spezi instance. - public var spezi: Spezi { // TODO: example via @Application property wrapper, docs + /// + /// Access the global Spezi instance using the ``Module/Application`` property wrapper inside your ``Module``. + /// + /// Below is a short code example on how to access the Spezi instance. + /// + /// ```swift + /// class ExampleModule: Module { + /// @Application(\.spezi) + /// var spezi + /// } + /// ``` + public var spezi: Spezi { // this seems nonsensical, but is essential to support Spezi access from the @Application modifier self } @@ -142,20 +171,23 @@ public class Spezi { self.loadModules([self.standard] + modules) } + /// Load a new Module. + /// + /// Loads a new Spezi ``Module`` resolving all dependencies. + /// - Note: Trying to load the same ``Module`` instance multiple times results in a runtime crash. + /// + /// - Parameter module: The new Module instance to load. public func loadModule(_ module: any Module) { loadModules([module]) } - private func loadModules(_ modules: [any Module]) { // TODO: docs + private func loadModules(_ modules: [any Module]) { let existingModules = self.modules let dependencyManager = DependencyManager(modules, existing: existingModules) dependencyManager.resolve() - // TODO: make sure this has Set semantics! - var implicitlyCreatedModules = storage[ModuleReferences.self] - implicitlyCreatedModules.append(contentsOf: dependencyManager.implicitlyCreatedModules) - storage[ModuleReferences.self] = implicitlyCreatedModules + implicitlyCreatedModules.formUnion(dependencyManager.implicitlyCreatedModules) for module in dependencyManager.initializedModules { // we pass through the whole list of modules once to collect all @Provide values @@ -175,7 +207,16 @@ public class Spezi { } } - public func unloadModule(_ module: any Module) { // TODO: docs + /// Unload a Module. + /// + /// Unloads a ``Module`` from the Spezi system. + /// - Important: Unloading a ``Module`` that is still required by other modules results in a runtime crash. + /// However, unloading a Module that is the **optional** dependency of another Module works. + /// + /// Unloading a Module will recursively unload its dependencies that were not loaded explicitly. + /// + /// - Parameter module: The Module to unload. + public func unloadModule(_ module: any Module) { guard module.isLoaded(in: self) else { return // module is not loaded } @@ -185,12 +226,7 @@ public class Spezi { module.clearModule(from: self) - // TODO: this is complicated boi!! - if storage[ModuleReferences.self].contains(ModuleReference(module)) { - var implicitlyCreatedModules = storage[ModuleReferences.self] - implicitlyCreatedModules.removeAll(where: { $0 == ModuleReference(module) }) - storage[ModuleReferences.self] = implicitlyCreatedModules - } + implicitlyCreatedModules.remove(ModuleReference(module)) // TODO: remove @Collect values that were previously provided by this Module @@ -206,9 +242,9 @@ public class Spezi { for dependencyDeclaration in module.dependencyDeclarations { let dependencies = dependencyDeclaration.injectedDependencies for dependency in dependencies { - // TODO: accessor naming! - guard storage[ModuleReferences.self].contains(ModuleReference(dependency)) else { - continue // TODO: docs! + guard implicitlyCreatedModules.contains(ModuleReference(dependency)) else { + // we only recursively unload modules that have been created implicitly + continue } guard retrieveDependingModules(dependency).isEmpty else { @@ -224,11 +260,9 @@ public class Spezi { /// /// Call this method to initialize a Module, injecting necessary information into Spezi property wrappers. /// - /// /// - Parameters: - /// - module: - /// - standard: - private func initModule(_ module: any Module) { // TODO: docs + /// - module: The module to initialize. + private func initModule(_ module: any Module) { precondition(!module.isLoaded(in: self), "Tried to initialize Module \(type(of: module)) that was already loaded!") Self.$moduleInitContext.withValue(module) { diff --git a/Sources/Spezi/Spezi/View+Spezi.swift b/Sources/Spezi/Spezi/View+Spezi.swift index 9babfe89..e0972ae4 100644 --- a/Sources/Spezi/Spezi/View+Spezi.swift +++ b/Sources/Spezi/Spezi/View+Spezi.swift @@ -12,7 +12,6 @@ import SwiftUI struct SpeziViewModifier: ViewModifier { @State private var spezi: Spezi - // let speziViewModifiers: [any ViewModifier] // TODO: this can now change! init(_ spezi: Spezi) { diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index c12599fc..3e757a8d 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -122,6 +122,9 @@ final class DependencyTests: XCTestCase { XCTAssert(testModule1.testModule2.testModule3 === testModule3) XCTAssert(testModule1.testModule2.testModule4.testModule5 === testModule5) XCTAssert(optionalModuleDependency.testModule3 === testModule3) + + // TODO: test updated view modifiers + // TODO: test updated @Collect properties! } func testUnloadingDependencies() throws { diff --git a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift index c6284b02..1a7782da 100644 --- a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift +++ b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift @@ -88,8 +88,13 @@ final class ModuleBuilderTests: XCTestCase { condition: true, expectations: expectations ) - - _ = Spezi(standard: MockStandard(), modules: modules.elements) + + let dependencyManager = DependencyManager(modules.elements) + dependencyManager.resolve() + for module in dependencyManager.initializedModules { + module.configure() + } + try expectations.wait() } @@ -105,7 +110,12 @@ final class ModuleBuilderTests: XCTestCase { expectations: expectations ) - _ = Spezi(standard: MockStandard(), modules: modules.elements) + let dependencyManager = DependencyManager(modules.elements) + dependencyManager.resolve() + for module in dependencyManager.initializedModules { + module.configure() + } + try expectations.wait() } }