diff --git a/Sources/Spezi/Capabilities/Communication/CollectPropertyWrapper.swift b/Sources/Spezi/Capabilities/Communication/CollectPropertyWrapper.swift index d3d91830..4f33cf5e 100644 --- a/Sources/Spezi/Capabilities/Communication/CollectPropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/Communication/CollectPropertyWrapper.swift @@ -65,7 +65,7 @@ extension Module { } -extension _CollectPropertyWrapper: _StorageValueCollector { +extension _CollectPropertyWrapper: StorageValueCollector { public func retrieve>(from repository: Repository) { injectedValues = repository[CollectedModuleValue.self] ?? [] } diff --git a/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift b/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift index cc235b4c..1704399b 100644 --- a/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift @@ -109,7 +109,7 @@ extension Module { } -extension _ProvidePropertyWrapper: _StorageValueProvider { +extension _ProvidePropertyWrapper: StorageValueProvider { public func collect>(into repository: inout Repository) { if let wrapperWithOptional = self as? OptionalBasedProvideProperty { wrapperWithOptional.collectOptional(into: &repository) diff --git a/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift b/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift index fbedd507..0aabe33b 100644 --- a/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift +++ b/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift @@ -13,10 +13,7 @@ import SpeziFoundation /// data provided by other ``Module``s. /// /// Data requested through a Storage Value Collector might be provided through a ``_StorageValueProvider``. -public protocol _StorageValueCollector { - // swiftlint:disable:previous type_name - // to be hidden from documentation - +protocol StorageValueCollector { /// This method is called to retrieve all the requested values from the given ``SpeziStorage`` repository. /// - Parameter repository: Provides access to the ``SpeziStorage`` repository for read access. func retrieve>(from repository: Repository) @@ -24,8 +21,8 @@ public protocol _StorageValueCollector { extension Module { - var storageValueCollectors: [_StorageValueCollector] { - retrieveProperties(ofType: _StorageValueCollector.self) + var storageValueCollectors: [StorageValueCollector] { + retrieveProperties(ofType: StorageValueCollector.self) } func injectModuleValues>(from repository: Repository) { diff --git a/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift b/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift index 6277119d..846eab7e 100644 --- a/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift +++ b/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift @@ -13,10 +13,7 @@ import SpeziFoundation /// data with other ``Module``s. /// /// Data provided through a Storage Value Provider can be retrieved through a ``_StorageValueCollector``. -public protocol _StorageValueProvider { - // swiftlint:disable:previous type_name - // to be hidden from documentation - +protocol StorageValueProvider { /// This method is called to collect all provided values into the given ``SpeziStorage`` repository. /// - Parameter repository: Provides access to the ``SpeziStorage`` repository. func collect>(into repository: inout Repository) @@ -24,8 +21,8 @@ public protocol _StorageValueProvider { extension Module { - var storageValueProviders: [_StorageValueProvider] { - retrieveProperties(ofType: _StorageValueProvider.self) + var storageValueProviders: [StorageValueProvider] { + retrieveProperties(ofType: StorageValueProvider.self) } func collectModuleValues>(into repository: inout Repository) { diff --git a/Sources/Spezi/Dependencies/DependencyBuilder.swift b/Sources/Spezi/Dependencies/DependencyBuilder.swift new file mode 100644 index 00000000..4cdaf7c0 --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyBuilder.swift @@ -0,0 +1,49 @@ +// +// 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 +// + + +/// A result builder to build a ``DependencyCollection``. +@resultBuilder +public enum DependencyBuilder { + /// An auto-closure expression, providing the default dependency value, building the ``DependencyCollection``. + public static func buildExpression(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection { + DependencyCollection(DependencyContext(defaultValue: expression)) + } + + /// Build a block of ``DependencyCollection``s. + public static func buildBlock(_ components: DependencyCollection...) -> DependencyCollection { + buildArray(components) + } + + /// Build the first block of an conditional ``DependencyCollection`` component. + public static func buildEither(first component: DependencyCollection) -> DependencyCollection { + component + } + + /// Build the second block of an conditional ``DependencyCollection`` component. + public static func buildEither(second component: DependencyCollection) -> DependencyCollection { + component + } + + /// Build an optional ``DependencyCollection`` component. + public static func buildOptional(_ component: DependencyCollection?) -> DependencyCollection { + component ?? DependencyCollection() + } + + /// Build an ``DependencyCollection`` component with limited availability. + public static func buildLimitedAvailability(_ component: DependencyCollection) -> DependencyCollection { + component + } + + /// Build an array of ``DependencyCollection`` components. + public static func buildArray(_ components: [DependencyCollection]) -> DependencyCollection { + DependencyCollection(components.reduce(into: []) { result, component in + result.append(contentsOf: component.entries) + }) + } +} diff --git a/Sources/Spezi/Dependencies/DependencyCollection.swift b/Sources/Spezi/Dependencies/DependencyCollection.swift new file mode 100644 index 00000000..add38cdb --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyCollection.swift @@ -0,0 +1,57 @@ +// +// 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 +// + + +/// A collection of dependency declarations. +public struct DependencyCollection: DependencyDeclaration { + let entries: [AnyDependencyContext] + + + init(_ entries: [AnyDependencyContext]) { + self.entries = entries + } + + init(_ entries: AnyDependencyContext...) { + self.init(entries) + } + + + func collect(into dependencyManager: DependencyManager) { + for entry in entries { + entry.collect(into: dependencyManager) + } + } + + func inject(from dependencyManager: DependencyManager) { + for entry in entries { + entry.inject(from: dependencyManager) + } + } + + private func singleDependencyContext() -> AnyDependencyContext { + guard let dependency = entries.first else { + preconditionFailure("DependencyCollection unexpectedly empty!") + } + precondition(entries.count == 1, "Expected exactly one element in the dependency collection!") + return dependency + } + + func singleDependencyRetrieval(for module: M.Type = M.self) -> M { + singleDependencyContext().retrieve(dependency: M.self) + } + + func singleOptionalDependencyRetrieval(for module: M.Type = M.self) -> M? { + singleDependencyContext().retrieveOptional(dependency: M.self) + } + + func retrieveModules() -> [any Module] { + entries.map { dependency in + dependency.retrieve(dependency: (any Module).self) + } + } +} diff --git a/Sources/Spezi/Dependencies/DependencyContext.swift b/Sources/Spezi/Dependencies/DependencyContext.swift new file mode 100644 index 00000000..a7b34724 --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyContext.swift @@ -0,0 +1,57 @@ +// +// 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 XCTRuntimeAssertions + + +protocol AnyDependencyContext: DependencyDeclaration { + func retrieve(dependency: M.Type) -> M + + func retrieveOptional(dependency: M.Type) -> M? +} + + +class DependencyContext: AnyDependencyContext { + let defaultValue: (() -> Dependency)? + private var injectedDependency: Dependency? + + init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) { + self.defaultValue = defaultValue + } + + 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) + } + + func retrieve(dependency: M.Type) -> M { + guard let injectedDependency else { + preconditionFailure( + """ + A `@Dependency` was accessed before the dependency was activated. \ + Only access dependencies once the module has been configured and the Spezi initialization is complete. + """ + ) + } + guard let dependency = injectedDependency 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 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/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/DependencyDeclaration.swift new file mode 100644 index 00000000..c7e21ed2 --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyDeclaration.swift @@ -0,0 +1,24 @@ +// +// 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 +// + + +/// Provides mechanism to communicate dependency requirements. +/// +/// This protocol allows to communicate dependency requirements of a ``Module`` to the ``DependencyManager``. +protocol DependencyDeclaration { + /// 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) +} + +extension Module { + var dependencyDeclarations: [DependencyDeclaration] { + retrieveProperties(ofType: DependencyDeclaration.self) + } +} diff --git a/Sources/Spezi/Dependencies/DependencyDescriptor.swift b/Sources/Spezi/Dependencies/DependencyDescriptor.swift deleted file mode 100644 index a32b3b5f..00000000 --- a/Sources/Spezi/Dependencies/DependencyDescriptor.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// The ``DependencyDescriptor`` protocol is the base for any property wrapper used to describe ``Module`` dependencies. -/// It is generally not needed to implement types conforming to ``DependencyDescriptor`` when using Spezi. -/// -/// Refer to the ``Module/Dependency`` and ``Module/DynamicDependencies`` property wrappers for more information. -public protocol DependencyDescriptor { - /// Used by the ``DependencyManager`` to gather dependency information. - func gatherDependency(dependencyManager: DependencyManager) - /// Used by the ``DependencyManager`` to inject resolved dependency information into a ``DependencyDescriptor``. - func inject(dependencyManager: DependencyManager) -} - - -extension Module { - var dependencyDescriptors: [any DependencyDescriptor] { - retrieveProperties(ofType: (any DependencyDescriptor).self) - } -} diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 4ec5984e..46108bb5 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -16,14 +16,14 @@ public class DependencyManager { /// 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. - private var recursiveSearch: [any Module] = [] - - + 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.dependencyDescriptors.isEmpty } - modulesWithDependencies = module.filter { !$0.dependencyDescriptors.isEmpty } + sortedModules = module.filter { $0.dependencyDeclarations.isEmpty } + modulesWithDependencies = module.filter { !$0.dependencyDeclarations.isEmpty } } @@ -37,43 +37,42 @@ public class DependencyManager { } for module in sortedModules { - for dependency in module.dependencyDescriptors { - dependency.inject(dependencyManager: self) + for dependency in module.dependencyDeclarations { + dependency.inject(from: self) } } } - - /// Injects a dependency into a `_DependencyPropertyWrapper` that is resolved in the `sortedModules`. - /// - Parameters: - /// - dependencyType: The type of the dependency that should be injected. - /// - anyDependency: The ``ModuleDependency`` that the provided dependency should be injected into. - func inject( - _ dependencyType: D.ModuleType.Type, - into anyDependency: D - ) { - guard let foundInSortedModuless = sortedModules.first(where: { type(of: $0) == D.ModuleType.self }) as? D.ModuleType else { - preconditionFailure("Could not find the injectable module in the `sortedModules`.") + + /// Push a module on the search stack and resolve dependency information. + private func push(_ module: any Module) { + searchStack.append(module) + for dependency in module.dependencyDeclarations { + dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)` } - - anyDependency.inject(dependency: foundInSortedModuless) + resolvedAllDependencies(module) } - + /// Communicate a requirement to a `DependencyManager` /// - Parameters: - /// - dependencyType: The type of the dependency that should be resolved. + /// - 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`. - func require(_ dependencyType: M.Type, defaultValue: @autoclosure () -> M) { + func require(_ 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 }) { return } // 2. Search for the required module is found in the `dependingModules` collection. - // If not, use the default value calling the `defaultValue` autoclosure. + // If not, use the default value calling the `defaultValue` auto-closure. guard let foundInModulesWithDependencies = modulesWithDependencies.first(where: { type(of: $0) == M.self }) else { + guard let defaultValue else { + // optional dependency. The user didn't supply anything. So we can't deliver anything. + return + } + let newModule = defaultValue() - guard !newModule.dependencyDescriptors.isEmpty else { + guard !newModule.dependencyDeclarations.isEmpty else { sortedModules.append(newModule) return } @@ -85,14 +84,14 @@ public class DependencyManager { } // Detect circles in the `recursiveSearch` collection. - guard !recursiveSearch.contains(where: { type(of: $0) == M.self }) else { - let dependencyChain = recursiveSearch + guard !searchStack.contains(where: { type(of: $0) == M.self }) else { + let dependencyChain = searchStack .map { String(describing: type(of: $0)) } .joined(separator: ", ") // The last element must exist as we entered the statement using a successful `contains` statement. // There is not chance to recover here: If there is a crash here, we would fail in the precondition statement in the next line anyways - let lastElement = recursiveSearch.last! // swiftlint:disable:this force_unwrapping + let lastElement = searchStack.last! // swiftlint:disable:this force_unwrapping preconditionFailure( """ The `DependencyManager` has detected a dependency cycle of your Spezi modules. @@ -106,12 +105,26 @@ public class DependencyManager { // If there is no cycle, resolved the dependencies of the module found in the `dependingModules`. push(foundInModulesWithDependencies) } + + /// Retrieve a resolved dependency for a given type. + /// + /// - Parameters: + /// - module: The ``Module`` type to return. + /// - optional: Flag indicating if it is a optional return. + func retrieve(module: M.Type = M.self, optional: Bool = false) -> M? { + guard let module = sortedModules.first(where: { $0 is M }) as? M else { + precondition(optional, "Could not located dependency of type \(M.self)!") + return nil + } + + return module + } private func resolvedAllDependencies(_ dependingModule: any Module) { - guard !recursiveSearch.isEmpty else { + guard !searchStack.isEmpty else { preconditionFailure("Internal logic error in the `DependencyManager`") } - let module = recursiveSearch.removeLast() + let module = searchStack.removeLast() guard module === dependingModule else { preconditionFailure("Internal logic error in the `DependencyManager`") @@ -128,17 +141,8 @@ public class DependencyManager { sortedModules.append(dependingModule) // Call the dependency resolution mechanism on the next element in the `dependingModules` if we are not in a recursive search. - if recursiveSearch.isEmpty, let nextModule = modulesWithDependencies.first { + if searchStack.isEmpty, let nextModule = modulesWithDependencies.first { push(nextModule) } } - - - private func push(_ module: any Module) { - recursiveSearch.append(module) - for dependency in module.dependencyDescriptors { - dependency.gatherDependency(dependencyManager: self) - } - resolvedAllDependencies(module) - } } diff --git a/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift new file mode 100644 index 00000000..e40b7d7f --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift @@ -0,0 +1,112 @@ +// +// 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 +import XCTRuntimeAssertions + + +/// A `@Dependency` for a single, typed ``Module``. +private protocol SingleModuleDependency { + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue +} + + +/// A `@Dependency` for a single, optional ``Module``. +private protocol OptionalModuleDependency { + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue +} + + +/// A `@Dependency` for an array of ``Module``s. +private protocol ModuleArrayDependency { + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue +} + + +/// Refer to the documentation of ``Module/Dependency`` for information on how to use the `@Dependency` property wrapper. +@propertyWrapper +public class _DependencyPropertyWrapper: DependencyDeclaration { // swiftlint:disable:this type_name + private let dependencies: DependencyCollection + + public var wrappedValue: Value { + if let singleModule = self as? SingleModuleDependency { + return singleModule.wrappedValue(as: Value.self) + } else if let moduleArray = self as? ModuleArrayDependency { + return moduleArray.wrappedValue(as: Value.self) + } else if let optionalModule = self as? OptionalModuleDependency { + return optionalModule.wrappedValue(as: Value.self) + } + + preconditionFailure("Reached inconsistent state. Wrapped value must be of type `Module`, `Module?` or `[any Module]`!") + } + + + fileprivate init(_ dependencies: DependencyCollection) { + self.dependencies = dependencies + } + + public convenience init() where Value: Module & DefaultInitializable { + // this init is placed here directly, otherwise Swift has problems resolving this init + self.init(wrappedValue: Value()) + } + + + func collect(into dependencyManager: DependencyManager) { + dependencies.collect(into: dependencyManager) + } + + func inject(from dependencyManager: DependencyManager) { + dependencies.inject(from: dependencyManager) + } +} + + +extension _DependencyPropertyWrapper: SingleModuleDependency where Value: Module { + public convenience init(wrappedValue defaultValue: @escaping @autoclosure () -> Value) { + self.init(DependencyCollection(DependencyContext(defaultValue: defaultValue))) + } + + + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + dependencies.singleDependencyRetrieval() + } +} + + +extension _DependencyPropertyWrapper: OptionalModuleDependency where Value: AnyOptional, Value.Wrapped: Module { + public convenience init() { + self.init(DependencyCollection(DependencyContext(for: Value.Wrapped.self))) + } + + + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + guard let value = dependencies.singleOptionalDependencyRetrieval(for: Value.Wrapped.self) as? WrappedValue else { + preconditionFailure("Failed to convert from Optional<\(Value.Wrapped.self)> to \(WrappedValue.self)") + } + return value + } +} + + +extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any Module] { + public convenience init() { + self.init(DependencyCollection()) + } + + + public convenience init(@DependencyBuilder _ dependencies: () -> DependencyCollection) { + self.init(dependencies()) + } + + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + guard let modules = dependencies.retrieveModules() as? WrappedValue else { + preconditionFailure("\(WrappedValue.self) doesn't match expected type \(Value.self)") + } + return modules + } +} diff --git a/Sources/Spezi/Dependencies/DependenyPropertyWrapper.swift b/Sources/Spezi/Dependencies/DependenyPropertyWrapper.swift deleted file mode 100644 index 7efec002..00000000 --- a/Sources/Spezi/Dependencies/DependenyPropertyWrapper.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import XCTRuntimeAssertions - - -/// Refer to ``Module/Dependency`` for information on how to use the `@Dependency` property wrapper. Do not use the `_DependencyPropertyWrapper` directly. -@propertyWrapper -public class _DependencyPropertyWrapper: ModuleDependency { - // swiftlint:disable:previous type_name - // We want the _DependencyPropertyWrapper type to be hidden from autocompletion and document generation. - - public let defaultValue: () -> M - private var dependency: M? - - - /// The dependency that is resolved by ``Spezi`` - public var wrappedValue: M { - guard let dependency else { - preconditionFailure( - """ - A `_DependencyPropertyWrapper`'s wrappedValue was accessed before the dependency was activated. - Only access dependencies once the module has been configured and the Spezi initialization is complete. - """ - ) - } - return dependency - } - - - /// Refer to ``Module/Dependency`` for information on how to use the `@Dependency` property wrapper. Do not use the `_DependencyPropertyWrapper` directly. - public init(wrappedValue defaultValue: @escaping @autoclosure () -> M) { - self.defaultValue = defaultValue - } - - - /// Refer to ``Module/Dependency`` for information on how to use the `@Dependency` property wrapper. Do not use the `_DependencyPropertyWrapper` directly. - public init() where M: DefaultInitializable { - self.defaultValue = { M() } - } - - - public func gatherDependency(dependencyManager: DependencyManager) { - dependencyManager.require(M.self, defaultValue: defaultValue()) - } - - public func inject(dependencyManager: DependencyManager) { - dependencyManager.inject(M.self, into: self) - } - - public func inject(dependency: M) { - precondition( - self.dependency == nil, - "Already injected a module: \(String(describing: dependency))" - ) - self.dependency = dependency - } -} diff --git a/Sources/Spezi/Dependencies/DynamicDependenciesPropertyWrapper.swift b/Sources/Spezi/Dependencies/DynamicDependenciesPropertyWrapper.swift deleted file mode 100644 index 65ecdf4e..00000000 --- a/Sources/Spezi/Dependencies/DynamicDependenciesPropertyWrapper.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// Refer to ``Module/DynamicDependencies`` for information on how to use the `@DynamicDependencies` property wrapper. -/// Do not use the `_DynamicDependenciesPropertyWrapper` directly. -@propertyWrapper -public class _DynamicDependenciesPropertyWrapper: DependencyDescriptor { - // swiftlint:disable:previous type_name - // We want the _DependencyPropertyWrapper type to be hidden from autocompletion and document generation. - - private var moduleProperties: [any ModuleDependency] - - - public var wrappedValue: [any Module] { - moduleProperties.map { - $0.wrappedValue as any Module - } - } - - - /// Refer to ``Module/DynamicDependencies`` for information on how to use the `@DynamicDependencies` property wrapper. - /// Do not use the `_DynamicDependenciesPropertyWrapper` directly. - public init(moduleProperties: @escaping @autoclosure () -> [any ModuleDependency]) { - self.moduleProperties = moduleProperties() - } - - - public func gatherDependency(dependencyManager: DependencyManager) { - for moduleProperty in moduleProperties { - gather(moduleProperty, in: dependencyManager) - } - } - - private func gather(_ moduleProperty: D, in dependencyManager: DependencyManager) { - dependencyManager.require(D.ModuleType.self, defaultValue: moduleProperty.defaultValue()) - } - - public func inject(dependencyManager: DependencyManager) { - for moduleProperty in moduleProperties { - inject(dependencyManager: dependencyManager, into: moduleProperty) - } - } - - private func inject( - dependencyManager: DependencyManager, - into moduleProperty: D - ) { - dependencyManager.inject(D.ModuleType.self, into: moduleProperty) - } -} diff --git a/Sources/Spezi/Dependencies/Module+Dependencies.swift b/Sources/Spezi/Dependencies/Module+Dependencies.swift index 96e2aec1..bfebd3f7 100644 --- a/Sources/Spezi/Dependencies/Module+Dependencies.swift +++ b/Sources/Spezi/Dependencies/Module+Dependencies.swift @@ -8,52 +8,66 @@ extension Module { - /// Defines a dependency to another ``Module``. + /// Define dependency to other `Module`s. /// - /// A ``Module`` can define the dependencies using the @``Module/Dependency`` property wrapper. + /// You can use this property wrapper inside your `Module` to define dependencies to other ``Module``s. /// /// - Note: You can access the contents of `@Dependency` once your ``Module/configure()-5pa83`` method is called (e.g., it must not be used in the `init`) - /// and can continue to access the Standard actor in methods like ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp``. + /// and can continue to access the dependency in methods like ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp``. + /// + /// The below code sample demonstrates a simple, singular dependence on the `ExampleModuleDependency` module. /// - /// The below code example demonstrates a simple dependence on the `ExampleModuleDependency` module. /// ```swift /// class ExampleModule: Module { - /// @Dependency var exampleModuleDependency = ExampleModuleDependency() + /// @Dependency var exampleDependency = ExampleModuleDependency() /// } /// ``` /// /// Some modules do not need a default value assigned to the property if they provide a default configuration and conform to ``DefaultInitializable``. /// ```swift /// class ExampleModule: Module { - /// @Dependency var exampleModuleDependency: ExampleModuleDependency + /// @Dependency var exampleDependency: ExampleModuleDependency /// } /// ``` - public typealias Dependency = _DependencyPropertyWrapper - - - /// Defines dynamic dependencies to other ``Module``s. /// - /// In contrast to the `@Dependency` property wrapper, the `@DynamicDependencies` enables the generation of the property wrapper in the initializer and generating an - /// arbitrary amount of dependencies that are resolved in the Spezi initialization. + /// ### Optional Dependencies + /// + /// You can define dependencies to be optional by declaring the `@Dependency` property wrapper optional. + /// The below code examples demonstrates this functionality. + /// + /// ```swift + /// class ExampleModule: Module { + /// @Dependency var exampleDependency: ExampleModuleDependency? + /// + /// func configure() { + /// if let exampleDependency { + /// // Dependency was defined by the user ... + /// } + /// } + /// } + /// ``` + /// + /// ### Computed Dependencies + /// + /// In certain circumstances your list of dependencies might not be statically known. Instead you might want to generate + /// your list of dependencies within the initializer, based on external factors. + /// The `@Dependency` property wrapper, allows you to define your dependencies using a result-builder-based appraoch. /// - /// - Note: You can access the contents of `@DynamicDependencies` once your ``Module/configure()-5pa83`` method is called (e.g., it must not be used in the `init`) - /// and can continue to access the Standard actor in methods like ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp``. + /// Below is a short code example that demonstrates this functionality. /// - /// A ``Module`` can define dynamic dependencies using the @``Module/DynamicDependencies`` property wrapper and can, e.g., initialize its value in the initializer. /// ```swift /// class ExampleModule: Module { - /// @DynamicDependencies var dynamicDependencies: [any Module] + /// @Dependency var computedDependencies: [any Module] /// /// /// init() { - /// self._dynamicDependencies = DynamicDependencies( - /// moduleProperty: [ - /// Dependency(wrappedValue: /* ... */), - /// Dependency(wrappedValue: /* ... */) - /// ] - /// ) + /// // a result builder to declare your module dependencies + /// self._computedDependencies = Dependency { + /// ModuleA() + /// ModuleB() + /// } /// } /// } /// ``` - public typealias DynamicDependencies = _DynamicDependenciesPropertyWrapper + public typealias Dependency = _DependencyPropertyWrapper } diff --git a/Sources/Spezi/Dependencies/ModuleDependency.swift b/Sources/Spezi/Dependencies/ModuleDependency.swift deleted file mode 100644 index 1f4f4745..00000000 --- a/Sources/Spezi/Dependencies/ModuleDependency.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// A ``ModuleDependency`` defines the interface of a ``Module/Dependency`` property wrapper. -public protocol ModuleDependency: DependencyDescriptor, AnyObject { - /// The type of the module that is defined as a dependency. - associatedtype ModuleType: Module - - - /// The default value defined by the ``Module`` describing the dependency. - var defaultValue: () -> ModuleType { get } - /// The resolved dependency provided by the ``Module/Dependency`` property wrapper. - var wrappedValue: ModuleType { get } - - - /// Inject a resolved ``ModuleDependency/ModuleType`` instance in the property wrapper. - func inject(dependency: ModuleType) -} diff --git a/Sources/Spezi/Module/Module.swift b/Sources/Spezi/Module/Module.swift index 96205919..0d09e8b5 100644 --- a/Sources/Spezi/Module/Module.swift +++ b/Sources/Spezi/Module/Module.swift @@ -14,7 +14,6 @@ import SpeziFoundation public protocol Module: AnyObject, KnowledgeSource { /// The ``Module/configure()-5pa83`` method is called on the initialization of the Spezi instance to perform a lightweight configuration of the module. /// - /// Both ``Module/Dependency`` and ``Module/DynamicDependencies`` are available and configured at this point. /// It is advised that longer setup tasks are done in an asynchronous task and started during the call of the configure method. func configure() } diff --git a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md index 8a0043e4..01d56c66 100644 --- a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md +++ b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md @@ -14,7 +14,7 @@ SPDX-License-Identifier: MIT ## Overview -``Module``s can define dependencies using the @``Module/Dependency`` property wrapper. +``Module``s can define dependencies using the @`Dependency` property wrapper. This establishes a strict order in which the ``Module/configure()-5pa83`` methods of each ``Module`` are called, to ensure functionality of a dependency is available at configuration. @@ -27,7 +27,7 @@ class ExampleModule: Module { } ``` -> Note: When the number of dependencies is dynamic, you might want to look at the ``Module/DynamicDependencies`` property wrapper. +> Note: Refer to the documentation of ``Module/Dependency`` if you need to dynamically compute your list of dependencies in the initializer. ### Default Initialization of Dependencies @@ -57,11 +57,13 @@ class ExampleModule: Module { ### Declaring Dependencies - ``Module/Dependency`` -- ``Module/DynamicDependencies`` ### Managing Dependencies - ``DefaultInitializable`` -- ``ModuleDependency`` - ``DependencyManager`` -- ``DependencyDescriptor`` + +### Building Dependencies + +- ``DependencyBuilder`` +- ``DependencyCollection`` diff --git a/Tests/SpeziTests/DependenciesTests/DependencyPropertyWrapperTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift similarity index 70% rename from Tests/SpeziTests/DependenciesTests/DependencyPropertyWrapperTests.swift rename to Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift index 2bc02c11..f03e248e 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyPropertyWrapperTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift @@ -9,8 +9,9 @@ import Spezi import XCTRuntimeAssertions +private final class ExampleModule: Module {} -final class DependencyDescriptorTests: XCTestCase { +final class DependencyContextTests: XCTestCase { func testInjectionPreconditionDependencyPropertyWrapper() throws { try XCTRuntimePrecondition { _ = _DependencyPropertyWrapper(wrappedValue: TestModule()).wrappedValue @@ -19,11 +20,10 @@ final class DependencyDescriptorTests: XCTestCase { func testInjectionPreconditionDynamicDependenciesPropertyWrapper() throws { try XCTRuntimePrecondition { - _ = _DynamicDependenciesPropertyWrapper( - moduleProperties: [ - _DependencyPropertyWrapper(wrappedValue: TestModule()) - ] - ).wrappedValue + _ = _DependencyPropertyWrapper { + ExampleModule() + } + .wrappedValue } } } diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index b59e5f16..780b0930 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -50,6 +50,11 @@ private final class TestModuleItself: Module { } +private final class OptionalModuleDependency: Module { + @Dependency var testModule3: TestModule3? +} + + final class DependencyTests: XCTestCase { func testModuleDependencyChain() throws { let modules: [any Module] = [ @@ -183,4 +188,32 @@ final class DependencyTests: XCTestCase { _ = DependencyManager.resolve(modules) } } + + func testOptionalDependenceNonPresent() throws { + let nonPresent: [any Module] = [ + OptionalModuleDependency() + ] + + let modules = DependencyManager.resolve(nonPresent) + + XCTAssertEqual(modules.count, 1) + + let module = try XCTUnwrap(modules[0] as? OptionalModuleDependency) + XCTAssertNil(module.testModule3) + } + + func testOptionalDependencePresent() throws { + let nonPresent: [any Module] = [ + OptionalModuleDependency(), + TestModule3() + ] + + let modules = DependencyManager.resolve(nonPresent) + + XCTAssertEqual(modules.count, 2) + + let module3 = try XCTUnwrap(modules[0] as? TestModule3) + let module = try XCTUnwrap(modules[1] as? OptionalModuleDependency) + XCTAssert(module.testModule3 === module3) + } } diff --git a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift index 92563192..13538dad 100644 --- a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift @@ -20,32 +20,26 @@ private enum DynamicDependenciesTestCase: CaseIterable { case dependencyCircle - var dynamicDependencies: _DynamicDependenciesPropertyWrapper { + var dynamicDependencies: _DependencyPropertyWrapper<[any Module]> { switch self { case .twoDependencies: - return _DynamicDependenciesPropertyWrapper( - moduleProperties: [ - _DependencyPropertyWrapper(wrappedValue: TestModule2()), - _DependencyPropertyWrapper(wrappedValue: TestModule3()) - ] - ) + return .init { + TestModule2() + TestModule3() + } case .duplicatedDependencies: - return _DynamicDependenciesPropertyWrapper( - moduleProperties: [ - _DependencyPropertyWrapper(wrappedValue: TestModule2()), - _DependencyPropertyWrapper(wrappedValue: TestModule3()), - _DependencyPropertyWrapper(wrappedValue: TestModule3()) - ] - ) + return .init { + TestModule2() + TestModule3() + TestModule3() + } case .noDependencies: - return _DynamicDependenciesPropertyWrapper(moduleProperties: []) + return .init() case .dependencyCircle: - return _DynamicDependenciesPropertyWrapper( - moduleProperties: [ - _DependencyPropertyWrapper(wrappedValue: TestModuleCircle1()), - _DependencyPropertyWrapper(wrappedValue: TestModuleCircle2()) - ] - ) + return .init { + TestModuleCircle1() + TestModuleCircle2() + } } } @@ -83,7 +77,7 @@ private enum DynamicDependenciesTestCase: CaseIterable { } private final class TestModule1: Module { - @DynamicDependencies var dynamicDependencies: [any Module] + @Dependency var dynamicDependencies: [any Module] let testCase: DynamicDependenciesTestCase