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 3 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 }}
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
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 Down Expand Up @@ -45,6 +50,19 @@ public struct DependencyCollection: DependencyDeclaration {
}


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

if relations.contains(.dependent) {
return .dependent
} else if relations.contains(.optional) {
return .optional
} else {
return .unrelated
}
}


func collect(into dependencyManager: DependencyManager) {
for entry in entries {
entry.collect(into: dependencyManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,39 @@ class DependencyContext<Dependency: Module>: AnyDependencyContext {
let defaultValue: (() -> Dependency)?
private var injectedDependency: Dependency?


var isOptional: Bool {
defaultValue == nil
}

var injectedDependencies: [any Module] {
injectedDependency.map { [$0] } ?? []
}

init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) {
self.defaultValue = defaultValue
}

func dependencyRelation(to module: any Module) -> DependencyRelation {
let type = type(of: module)

guard type == Dependency.self else {
return .unrelated
}

if isOptional {
return .optional
} else {
return .dependent
}
}

func collect(into dependencyManager: DependencyManager) {
dependencyManager.require(Dependency.self, defaultValue: defaultValue)
}

func inject(from dependencyManager: DependencyManager) {
precondition(injectedDependency == nil, "Dependency of type \(Dependency.self) is already injected!")
injectedDependency = dependencyManager.retrieve(optional: defaultValue == nil)
injectedDependency = dependencyManager.retrieve(optional: isOptional)
}

func retrieve<M>(dependency: M.Type) -> M {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,36 @@
// SPDX-License-Identifier: MIT
//

/// The relationship of given `Module` to another `Module`.
enum DependencyRelation: Hashable {
/// The given module is a dependency of the other module.
case dependent
/// The given module is an optional dependency of the other module.
case optional
/// The given module is not a dependency of the other module.
case unrelated
}


/// Provides mechanism to communicate dependency requirements.
///
/// 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:)`.
func collect(into dependencyManager: DependencyManager)
/// Inject the dependency instance from the ``DependencyManager``. Use `DependencyManager/retrieve(module:)`.
func inject(from dependencyManager: DependencyManager)

/// Determine the dependency relationship to a given module.
/// - Parameter module: The module to retrieve the dependency relationship for.
/// - Returns: Returns the `DependencyRelation`
func dependencyRelation(to module: any Module) -> DependencyRelation
}


extension Module {
var dependencyDeclarations: [DependencyDeclaration] {
retrieveProperties(ofType: DependencyDeclaration.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

/// Refer to the documentation of ``Module/Dependency`` for information on how to use the `@Dependency` property wrapper.
@propertyWrapper
public class _DependencyPropertyWrapper<Value>: DependencyDeclaration { // swiftlint:disable:this type_name
public class _DependencyPropertyWrapper<Value> { // swiftlint:disable:this type_name
private let dependencies: DependencyCollection

public var wrappedValue: Value {

Check failure on line 36 in Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 36 in Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 36 in Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)
if let singleModule = self as? SingleModuleDependency {
return singleModule.wrappedValue(as: Value.self)
} else if let moduleArray = self as? ModuleArrayDependency {
Expand All @@ -55,7 +55,18 @@
// this init is placed here directly, otherwise Swift has problems resolving this init
self.init(wrappedValue: Value())
}
}


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


func dependencyRelation(to module: any Module) -> DependencyRelation {
dependencies.dependencyRelation(to: module)
}

func collect(into dependencyManager: DependencyManager) {
dependencies.collect(into: dependencyManager)
Expand Down
Loading
Loading