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 dynamically load and unload Modules #105

Merged
merged 7 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ jobs:
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: Spezi-Package-iOS.xcresult Spezi-Package-watchOS.xcresult Spezi-Package-visionOS.xcresult Spezi-Package-tvOS.xcresult Spezi-Package-macOS.xcresult TestApp-iOS.xcresult TestApp-visionOS.xcresult
secrets:
token: ${{ secrets.CODECOV_TOKEN }}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ extension Module {

extension _CollectPropertyWrapper: StorageValueCollector {
public func retrieve<Repository: SharedRepository<SpeziAnchor>>(from repository: Repository) {
injectedValues = repository[CollectedModuleValue<Value>.self] ?? []
injectedValues = repository[CollectedModuleValues<Value>.self]?.map { $0.value } ?? []
}
}

This file was deleted.

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

import SpeziFoundation

protocol AnyCollectModuleValue {
associatedtype Value

var moduleReference: ModuleReference { get }
}

protocol AnyCollectModuleValues {
associatedtype Value

var values: [any AnyCollectModuleValue] { get }

mutating func removeValues(from module: any Module) -> Bool

func store(into storage: inout SpeziStorage)
}


struct CollectModuleValue<Value>: AnyCollectModuleValue {
let value: Value
let moduleReference: ModuleReference

init(_ value: Value) {
self.value = value

guard let module = Spezi.moduleInitContext else {
preconditionFailure("Tried to initialize CollectModuleValue with unknown module init context.")

Check warning on line 36 in Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift#L36

Added line #L36 was not covered by tests
}
self.moduleReference = ModuleReference(module)
}
}

/// Provides the ``KnowledgeSource`` for any value we store in the ``SpeziStorage`` that is
/// provided or request from am ``Module``.
///
/// For more information, look at the ``Module/Provide`` or ``Module/Collect`` property wrappers.
struct CollectedModuleValues<ModuleValue>: DefaultProvidingKnowledgeSource {
typealias Anchor = SpeziAnchor

typealias Value = [CollectModuleValue<ModuleValue>]


static var defaultValue: Value {
[]
}
}


extension Array: AnyCollectModuleValues where Element: AnyCollectModuleValue {
typealias Value = Element.Value

var values: [any AnyCollectModuleValue] {
self
}

Check warning on line 63 in Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift#L61-L63

Added lines #L61 - L63 were not covered by tests

mutating func removeValues(from module: any Module) -> Bool {
let previousCount = count
removeAll { entry in
entry.moduleReference == ModuleReference(module)
}
return previousCount != count
}

func store(into storage: inout SpeziStorage) {
guard let values = self as? [CollectModuleValue<Value>] else {
preconditionFailure("Unexpected array type: \(type(of: self))")

Check warning on line 75 in Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Communication/CollectedModuleValues.swift#L75

Added line #L75 was not covered by tests
}
storage[CollectedModuleValues<Value>.self] = values
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ extension _ProvidePropertyWrapper: StorageValueProvider {
} else if let wrapperWithArray = self as? CollectionBasedProvideProperty {
wrapperWithArray.collectArrayElements(into: &repository)
} else {
// concatenation is handled by the `CollectedModuleValue/reduce` implementation.
repository[CollectedModuleValue<Value>.self] = [storedValue]
repository.appendValues([CollectModuleValue(storedValue)])
}

collected = true
Expand All @@ -127,16 +126,24 @@ extension _ProvidePropertyWrapper: StorageValueProvider {

extension _ProvidePropertyWrapper: CollectionBasedProvideProperty where Value: AnyArray {
func collectArrayElements<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository) {
// concatenation is handled by the `CollectedModuleValue/reduce` implementation.
repository[CollectedModuleValue<Value.Element>.self] = storedValue.unwrappedArray
repository.appendValues(storedValue.unwrappedArray.map { CollectModuleValue($0) })
}
}


extension _ProvidePropertyWrapper: OptionalBasedProvideProperty where Value: AnyOptional {
func collectOptional<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository) {
if let storedValue = storedValue.unwrappedOptional {
repository[CollectedModuleValue<Value.Wrapped>.self] = [storedValue]
repository.appendValues([CollectModuleValue(storedValue)])
}
}
}


extension SharedRepository where Anchor == SpeziAnchor {
fileprivate mutating func appendValues<Value>(_ values: [CollectModuleValue<Value>]) {
var current = self[CollectedModuleValues<Value>.self]
current.append(contentsOf: values)
self[CollectedModuleValues<Value>.self] = current
}
}
96 changes: 61 additions & 35 deletions Sources/Spezi/Dependencies/DependencyManager.swift
Supereg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,59 @@
import XCTRuntimeAssertions


/// A ``DependencyManager`` in Spezi is used to gather information about modules with dependencies.
/// Gather information about modules with dependencies.
public class DependencyManager {
/// Collection of sorted modules after resolving all dependencies.
var sortedModules: [any Module]
/// Collection of already initialized modules.
private let existingModules: [any Module]

/// Collection of initialized Modules.
///
/// Order is determined by the dependency tree. This represents the result of the dependency resolution process.
private(set) var initializedModules: [any Module]
/// List of implicitly created Modules.
///
/// 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: Set<ModuleReference> = []

/// Collection of all modules with dependencies that are not yet processed.
private var modulesWithDependencies: [any Module]
/// Collection used to keep track of modules with dependencies in the recursive search.
/// Recursive search stack to keep track of potential circular dependencies.
private var searchStack: [any Module] = []


/// A ``DependencyManager`` in Spezi is used to gather information about modules with dependencies.
/// - Parameter module: The modules that should be resolved.
init(_ module: [any Module]) {
sortedModules = module.filter { $0.dependencyDeclarations.isEmpty }
modulesWithDependencies = module.filter { !$0.dependencyDeclarations.isEmpty }
///
/// - Parameters:
/// - modules: The modules that should be resolved.
/// - existingModules: Collection of already initialized modules.
init(_ modules: [any Module], existing existingModules: [any Module] = []) {
// modules without dependencies are already considered resolved
self.initializedModules = modules.filter { $0.dependencyDeclarations.isEmpty }

self.modulesWithDependencies = modules.filter { !$0.dependencyDeclarations.isEmpty }

self.existingModules = existingModules
}


/// Resolves the dependency order.
///
/// After calling `resolve` you can safely access `sortedModules`.
/// 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)
}

for module in sortedModules {
injectDependencies()
}

private func injectDependencies() {
// We inject dependencies into existingModules as well as a new dependency might be an optional dependency from a existing module
// that wasn't previously injected.
for module in initializedModules + existingModules {
for dependency in module.dependencyDeclarations {
dependency.inject(from: self)
}
Expand All @@ -49,19 +74,21 @@
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`
/// - Parameters:
/// - dependency: The type of the dependency that should be resolved.
/// - defaultValue: A default instance of the dependency that is used when the `dependencyType` is not present in the `sortedModules` or `modulesWithDependencies`.
/// - defaultValue: A default instance of the dependency that is used when the `dependencyType` is not present in the `initializedModules` or `modulesWithDependencies`.
func require<M: Module>(_ dependency: M.Type, defaultValue: (() -> M)?) {
// 1. Return if the depending module is found in the `sortedModules` collection.
if sortedModules.contains(where: { type(of: $0) == M.self }) {
// 1. Return if the depending module is found in the `initializedModules` collection.
if initializedModules.contains(where: { type(of: $0) == M.self })
|| existingModules.contains(where: { type(of: $0) == M.self }) {
return
}



// 2. Search for the required module is found in the `dependingModules` collection.
// If not, use the default value calling the `defaultValue` auto-closure.
guard let foundInModulesWithDependencies = modulesWithDependencies.first(where: { type(of: $0) == M.self }) else {
Expand All @@ -71,9 +98,11 @@
}

let newModule = defaultValue()


implicitlyCreatedModules.insert(ModuleReference(newModule))

guard !newModule.dependencyDeclarations.isEmpty else {
sortedModules.append(newModule)
initializedModules.append(newModule)
return
}

Expand Down Expand Up @@ -102,7 +131,7 @@
)
}

// If there is no cycle, resolved the dependencies of the module found in the `dependingModules`.
// If there is no cycle, resolve the dependencies of the module found in the `dependingModules`.
push(foundInModulesWithDependencies)
}

Expand All @@ -111,38 +140,35 @@
/// - Parameters:
/// - module: The ``Module`` type to return.
/// - 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 module = sortedModules.first(where: { $0 is M }) as? M else {
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 {
precondition(optional, "Could not located dependency of type \(M.self)!")
return nil
}

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`")
preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack is empty.")

Check warning on line 156 in Sources/Spezi/Dependencies/DependencyManager.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/DependencyManager.swift#L156

Added line #L156 was not covered by tests
}
let module = searchStack.removeLast()

guard module === dependingModule else {
preconditionFailure("Internal logic error in the `DependencyManager`")
preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack element was not the one we are resolving for.")

Check warning on line 161 in Sources/Spezi/Dependencies/DependencyManager.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/DependencyManager.swift#L161

Added line #L161 was not covered by tests
}


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`"
"Unexpected reduction of modules."
)

sortedModules.append(dependingModule)

// 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)
}
}
22 changes: 22 additions & 0 deletions Sources/Spezi/Dependencies/Module+DependencyRelation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// 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
//


extension Module {
func dependencyRelation(to module: any Module) -> DependencyRelation {
let relations = dependencyDeclarations.map { $0.dependencyRelation(to: module) }

if relations.contains(.dependent) {
return .dependent
} else if relations.contains(.optional) {
return .optional
} else {
return .unrelated
}
}
}
16 changes: 16 additions & 0 deletions Sources/Spezi/Dependencies/ModuleReference.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
//


struct ModuleReference: Hashable {
private let id: ObjectIdentifier

init(_ module: any Module) {
self.id = ObjectIdentifier(module)
}
}
Loading
Loading