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

Provide integration points for SpeziNotifications, Swift 6 and silence some warnings #117

Merged
merged 25 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b688b43
Allow `@Application` in SwiftUI views and additional notifications su…
Supereg Sep 13, 2024
c4cbf0c
Align the structure and update documentation
Supereg Sep 13, 2024
ba32953
Add LocalNotifications module
Supereg Sep 13, 2024
1194e2f
Compatibility with Swift 5 toolchain
Supereg Sep 13, 2024
2a9ddc7
More compatibility
Supereg Sep 13, 2024
a661cce
Support querying pending and delivered notifications
Supereg Sep 16, 2024
14513af
Update urls
Supereg Sep 17, 2024
ad6ffcd
Restore compatibility with other platforms
Supereg Sep 17, 2024
9b8f3e4
Enable Swift 6 language mode
Supereg Sep 17, 2024
c4aa84f
Remove latest workflows
Supereg Sep 17, 2024
9ac1bc1
Also apply to implementation
Supereg Sep 17, 2024
3c9a92f
Fix Swift 6 compatibility of unit tests
Supereg Sep 17, 2024
d89a3a1
Use Swift 6 Language mode for UI tests as well
Supereg Sep 17, 2024
7039019
Be less restrictive
Supereg Sep 19, 2024
7f6a2b9
Retrieve spezi from the view environment
Supereg Sep 20, 2024
443bf5c
Remove Notification realted infrastructure again. Moving to SpeziNoti…
Supereg Sep 28, 2024
e55c7b5
Minor changes
Supereg Sep 28, 2024
cbbe126
Minor fixes
Supereg Sep 28, 2024
c73eaaa
Regactor some infrastructure
Supereg Sep 30, 2024
f502ed2
Make APISupport Spezi access static
Supereg Sep 30, 2024
6aa8e4d
Some swiftlint
Supereg Sep 30, 2024
dd59b84
Docs changes
Supereg Oct 14, 2024
3752b6d
Introduce DependencyManagerError
Supereg Oct 28, 2024
b536a87
Introduce error hierarchy
Supereg Oct 28, 2024
b723a0a
Add links
Supereg Oct 28, 2024
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
49 changes: 20 additions & 29 deletions Sources/Spezi/Dependencies/DependencyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ public class DependencyManager: Sendable {
/// Resolves the dependency order.
///
/// After calling `resolve()` you can safely access `initializedModules`.
func resolve() {
func resolve() throws(DependencyManagerError) {
while let nextModule = modulesWithDependencies.first {
push(nextModule)
try push(nextModule)
}

injectDependencies()
try injectDependencies()
assert(searchStacks.isEmpty, "`searchStacks` are not getting cleaned up!")
assert(currentPushedModule == nil, "`currentPushedModule` is never reset!")
assert(modulesWithDependencies.isEmpty, "modulesWithDependencies has remaining entries \(modulesWithDependencies)")
Expand Down Expand Up @@ -109,26 +109,26 @@ public class DependencyManager: Sendable {
return order
}

private func injectDependencies() {
private func injectDependencies() throws(DependencyManagerError) {
// 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, for: module)
try dependency.inject(from: self, for: module)
}
}
}

/// Push a module on the search stack and resolve dependency information.
private func push(_ module: any Module) {
private func push(_ module: any Module) throws(DependencyManagerError) {
assert(currentPushedModule == nil, "Module already pushed. Did the algorithm turn into an recursive one by accident?")

currentPushedModule = ModuleReference(module)
searchStacks[ModuleReference(module), default: []]
.append(type(of: module))

for dependency in module.dependencyDeclarations {
dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)`
try dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)`
}

finishSearch(for: module)
Expand All @@ -138,8 +138,8 @@ public class DependencyManager: Sendable {
/// - 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 `initializedModules` or `modulesWithDependencies`.
func require<M: Module>(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) {
testForSearchStackCycles(M.self)
func require<M: Module>(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) throws(DependencyManagerError) {
try testForSearchStackCycles(M.self)

// 1. Check if it is actively requested to load this module.
if case .load = dependencyType {
Expand Down Expand Up @@ -177,18 +177,13 @@ public class DependencyManager: Sendable {
/// - 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, type dependencyType: DependencyType, for owner: any Module) -> M? {
func retrieve<M: Module>(module: M.Type, type dependencyType: DependencyType, for owner: any Module) throws(DependencyManagerError) -> M? {
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(
dependencyType.isOptional,
"""
'\(type(of: owner)) requires dependency of type '\(M.self)' which wasn't configured.
Please make sure this module is configured by including it in the configuration of your `SpeziAppDelegate` or following \
Module-specific instructions.
"""
)
if !dependencyType.isOptional {
throw DependencyManagerError.missingRequiredModule(module: "\(type(of: owner))", requiredModule: "\(M.self)")
}
return nil
}

Expand Down Expand Up @@ -231,20 +226,16 @@ public class DependencyManager: Sendable {
searchStacks[ModuleReference(module)] = searchStack
}

private func testForSearchStackCycles<M>(_ module: M.Type) {
private func testForSearchStackCycles<M>(_ module: M.Type) throws(DependencyManagerError) {
if let currentPushedModule {
let searchStack = searchStacks[currentPushedModule, default: []]

precondition(
!searchStack.contains(where: { $0 == M.self }),
"""
The `DependencyManager` has detected a dependency cycle of your Spezi modules.
The current dependency chain is: \(searchStack.map { String(describing: $0) }.joined(separator: ", ")). \
The module '\(searchStack.last.unsafelyUnwrapped)' required '\(M.self)' which is contained in its own dependency chain.

Please ensure that the modules you use or develop can not trigger a dependency cycle.
"""
)
if searchStack.contains(where: { $0 == M.self }) {
let module = "\(searchStack.last.unsafelyUnwrapped)"
let dependencyChain = searchStack
.map { String(describing: $0) }
throw DependencyManagerError.searchStackCycle(module: module, requestedModule: "\(M.self)", dependencyChain: dependencyChain)
}
}
}
}
35 changes: 35 additions & 0 deletions Sources/Spezi/Dependencies/DependencyManagerError.swift
Original file line number Diff line number Diff line change
@@ -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
//


enum DependencyManagerError: Error {
case searchStackCycle(module: String, requestedModule: String, dependencyChain: [String])
case missingRequiredModule(module: String, requiredModule: String)
}


extension DependencyManagerError: CustomStringConvertible {
var description: String {
switch self {
case let .searchStackCycle(module, requestedModule, dependencyChain):
"""
The `DependencyManager` has detected a dependency cycle of your Spezi modules.
The current dependency chain is: \(dependencyChain.joined(separator: ", ")). \
The module '\(module)' required '\(requestedModule)' which is contained in its own dependency chain.

Please ensure that the modules you use or develop can not trigger a dependency cycle.
"""

Check warning on line 26 in Sources/Spezi/Dependencies/DependencyManagerError.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Dependencies/DependencyManagerError.swift#L20-L26

Added lines #L20 - L26 were not covered by tests
case let .missingRequiredModule(module, requiredModule):
"""
'\(module) requires dependency of type '\(requiredModule)' which wasn't configured.
Please make sure this module is configured by including it in the configuration of your `SpeziAppDelegate` or following \
Module-specific instructions.
"""
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ extension DependencyCollection: DependencyDeclaration {
}


func collect(into dependencyManager: DependencyManager) {
func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) {
for entry in entries {
entry.collect(into: dependencyManager)
try entry.collect(into: dependencyManager)
}
}

func inject(from dependencyManager: DependencyManager, for module: any Module) {
func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) {
for entry in entries {
entry.inject(from: dependencyManager, for: module)
try entry.inject(from: dependencyManager, for: module)
}
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/Spezi/Dependencies/Property/DependencyContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ class DependencyContext<Dependency: Module>: AnyDependencyContext {
}
}

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

func inject(from dependencyManager: DependencyManager, for module: any Module) {
guard let dependency = dependencyManager.retrieve(module: Dependency.self, type: type, for: module) else {
func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) {
guard let dependency = try dependencyManager.retrieve(module: Dependency.self, type: type, for: module) else {
injectedDependency = nil
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ protocol DependencyDeclaration {

/// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`.
@MainActor
func collect(into dependencyManager: DependencyManager)
func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError)
/// Inject the dependency instance from the ``DependencyManager``. Use `DependencyManager/retrieve(module:)`.
/// - Parameters:
/// - dependencyManager: The dependency manager to inject the dependencies from.
/// - module: The module where the dependency declaration is located at.
@MainActor
func inject(from dependencyManager: DependencyManager, for module: any Module)
func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError)

@MainActor
func inject(spezi: Spezi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@ extension _DependencyPropertyWrapper: DependencyDeclaration {
dependencies.dependencyRelation(to: module)
}

func collect(into dependencyManager: DependencyManager) {
dependencies.collect(into: dependencyManager)
func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) {
try dependencies.collect(into: dependencyManager)
}

func inject(from dependencyManager: DependencyManager, for module: any Module) {
dependencies.inject(from: dependencyManager, for: module)
func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) {
try dependencies.inject(from: dependencyManager, for: module)
}

func uninjectDependencies(notifying spezi: Spezi) {
Expand Down
12 changes: 10 additions & 2 deletions Sources/Spezi/Spezi/Spezi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@
let existingModules = self.modules

let dependencyManager = DependencyManager(modules, existing: existingModules)
dependencyManager.resolve()
do {
try dependencyManager.resolve()
} catch {
preconditionFailure(error.description)

Check warning on line 233 in Sources/Spezi/Spezi/Spezi.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Spezi/Spezi.swift#L233

Added line #L233 was not covered by tests
}

implicitlyCreatedModules.formUnion(dependencyManager.implicitlyCreatedModules)

Expand Down Expand Up @@ -296,7 +300,11 @@
// re-injecting all dependencies ensures that the unloaded module is cleared from optional Dependencies from
// pre-existing Modules.
let dependencyManager = DependencyManager([], existing: modules)
dependencyManager.resolve()
do {
try dependencyManager.resolve()
} catch {
preconditionFailure("Internal inconsistency. Repeated dependency resolve resulted in error: \(error)")

Check warning on line 306 in Sources/Spezi/Spezi/Spezi.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Spezi/Spezi.swift#L306

Added line #L306 was not covered by tests
}

module.clear() // automatically removes @Provide values and recursively unloads implicitly created modules
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Spezi/Standard/StandardPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
func inject(spezi: Spezi) {
guard let standard = spezi.standard as? Constraint else {
let standardType = type(of: spezi.standard)
// TODO: allow this to get throwing!

Check failure on line 44 in Sources/Spezi/Standard/StandardPropertyWrapper.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (allow this to get throwing!) (todo)

Check warning on line 44 in Sources/Spezi/Standard/StandardPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Standard/StandardPropertyWrapper.swift#L44

Added line #L44 was not covered by tests
Supereg marked this conversation as resolved.
Show resolved Hide resolved
preconditionFailure(
"""
The `Standard` defined in the `Configuration` does not conform to \(String(describing: Constraint.self)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ final class DependencyBuilderTests: XCTestCase {
let module = ExampleDependencyModule {
ExampleDependentModule()
}
let initializedModules = DependencyManager.resolve([module])
let initializedModules = DependencyManager.resolveWithoutErrors([module])
XCTAssertEqual(initializedModules.count, 2)
_ = try XCTUnwrap(initializedModules[0] as? ExampleDependentModule)
_ = try XCTUnwrap(initializedModules[1] as? ExampleDependencyModule)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@
//

@testable import Spezi
import XCTest


extension DependencyManager {
static func resolve(_ modules: [any Module]) -> [any Module] {
static func resolve(_ modules: [any Module]) throws -> [any Module] {
let dependencyManager = DependencyManager(modules)
dependencyManager.resolve()
try dependencyManager.resolve()
return dependencyManager.initializedModules
}

static func resolveWithoutErrors(_ modules: [any Module], file: StaticString = #filePath, line: UInt = #line) -> [any Module] {
let dependencyManager = DependencyManager(modules)
XCTAssertNoThrow(try dependencyManager.resolve(), file: file, line: line)
return dependencyManager.initializedModules
}
}
Loading
Loading