From d56ed07327cce8254b919665eb61e20bfd1467b0 Mon Sep 17 00:00:00 2001 From: Philipp Zagar Date: Thu, 7 Dec 2023 10:59:55 -0800 Subject: [PATCH] Enforcement of customizable type constraints for DependencyBuilder (#93) --- CONTRIBUTORS.md | 1 + .../Dependencies/DependencyBuilder.swift | 34 +---------- .../Dependencies/DependencyCollection.swift | 24 ++++++++ .../DependencyCollectionBuilder.swift | 56 +++++++++++++++++++ .../DependencyPropertyWrapper.swift | 28 +++++++++- Sources/Spezi/Module/ModuleBuilder.swift | 2 +- .../Spezi.docc/Module/Module Dependency.md | 1 + .../DependencyBuilderTests.swift | 46 +++++++++++++++ 8 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 Sources/Spezi/Dependencies/DependencyCollectionBuilder.swift create mode 100644 Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6be62a01..dd065475 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,3 +14,4 @@ Spezi contributors * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) * [Vishnu Ravi](https://github.com/vishnuravi) * [Andreas Bauer](https://github.com/Supereg) +* [Philipp Zagar](https://github.com/philippzagar) diff --git a/Sources/Spezi/Dependencies/DependencyBuilder.swift b/Sources/Spezi/Dependencies/DependencyBuilder.swift index 4cdaf7c0..bcded6ea 100644 --- a/Sources/Spezi/Dependencies/DependencyBuilder.swift +++ b/Sources/Spezi/Dependencies/DependencyBuilder.swift @@ -9,41 +9,9 @@ /// A result builder to build a ``DependencyCollection``. @resultBuilder -public enum DependencyBuilder { +public enum DependencyBuilder: DependencyCollectionBuilder { /// 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 index add38cdb..46a71bb6 100644 --- a/Sources/Spezi/Dependencies/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/DependencyCollection.swift @@ -19,6 +19,30 @@ public struct DependencyCollection: DependencyDeclaration { init(_ entries: AnyDependencyContext...) { self.init(entries) } + + /// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``. + /// - Parameters: + /// - type: The generic type resulting from the passed closure, has to conform to ``Module``. + /// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``. + /// + /// ### Usage + /// + /// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module`) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder. + /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)``. + /// + /// ```swift + /// @resultBuilder + /// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder { + /// public static func buildExpression(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection { + /// DependencyCollection(singleEntry: expression) + /// } + /// } + /// ``` + /// + /// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder. + public init(for type: Dependency.Type = Dependency.self, singleEntry: @escaping (() -> Dependency)) { + self.init(DependencyContext(for: type, defaultValue: singleEntry)) + } func collect(into dependencyManager: DependencyManager) { diff --git a/Sources/Spezi/Dependencies/DependencyCollectionBuilder.swift b/Sources/Spezi/Dependencies/DependencyCollectionBuilder.swift new file mode 100644 index 00000000..f353e50f --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyCollectionBuilder.swift @@ -0,0 +1,56 @@ +// +// 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 protocol enabling the implementation of a result builder to build a ``DependencyCollection``. +/// Enables the simple construction of a result builder accepting ``Module``s with additional type constraints (useful for DSL implementations). +/// +/// Upon conformance, developers are required to implement a single result builder function transforming an arbitrary ``Module`` type constraint (M in the example below) to a ``DependencyCollection``. +/// All other result builder functions for constructing a ``DependencyCollection`` are provided as a default protocol implementation. +/// ```swift +/// static func buildExpression(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection +/// ``` +/// +/// See ``DependencyCollection/init(for:singleEntry:)`` for an example conformance implementation of the ``DependencyCollectionBuilder``. +public protocol DependencyCollectionBuilder {} + + +/// Default protocol implementations of a result builder constructing a ``DependencyCollection``. +extension DependencyCollectionBuilder { + /// 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/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift index e40b7d7f..ef4b756d 100644 --- a/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift @@ -97,11 +97,35 @@ extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any public convenience init() { self.init(DependencyCollection()) } - - + + /// Creates the `@Dependency` property wrapper from an instantiated ``DependencyCollection``, enabling the use of a custom ``DependencyBuilder`` enforcing certain type constraints on the passed, nested ``Dependency``s. + /// - Parameters: + /// - dependencies: The ``DependencyCollection`` to be wrapped. + /// + /// ### Usage + /// + /// The `ExampleModule` is initialized with nested ``Module/Dependency``s (``Module``s) enforcing certain type constraints via the `SomeCustomDependencyBuilder`. + /// Spezi automatically injects declared ``Dependency``s within the passed ``Dependency``s in the initializer, enabling proper nesting of ``Module``s. + /// + /// ```swift + /// class ExampleModule: Module { + /// @Dependency var dependentModules: [any Module] + /// + /// init(@SomeCustomDependencyBuilder _ dependencies: @Sendable () -> DependencyCollection) { + /// self._dependentModules = Dependency(using: dependencies()) + /// } + /// } + /// ``` + /// + /// See ``DependencyCollection/init(for:singleEntry:)`` for a continued example, specifically the implementation of the `SomeCustomDependencyBuilder` result builder. + public convenience init(using dependencies: DependencyCollection) { + self.init(dependencies) + } + public convenience init(@DependencyBuilder _ dependencies: () -> DependencyCollection) { self.init(dependencies()) } + func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { guard let modules = dependencies.retrieveModules() as? WrappedValue else { diff --git a/Sources/Spezi/Module/ModuleBuilder.swift b/Sources/Spezi/Module/ModuleBuilder.swift index 78a4b6eb..2010c62d 100644 --- a/Sources/Spezi/Module/ModuleBuilder.swift +++ b/Sources/Spezi/Module/ModuleBuilder.swift @@ -7,7 +7,7 @@ // -/// A function builder used to aggregate multiple `Module`s +/// A function builder used to aggregate multiple ``Module``s @resultBuilder public enum ModuleBuilder { /// If declared, provides contextual type information for statement expressions to translate them into partial results. diff --git a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md index 01d56c66..c9d8f8b8 100644 --- a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md +++ b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md @@ -66,4 +66,5 @@ class ExampleModule: Module { ### Building Dependencies - ``DependencyBuilder`` +- ``DependencyCollectionBuilder`` - ``DependencyCollection`` diff --git a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift new file mode 100644 index 00000000..bcb6d22b --- /dev/null +++ b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift @@ -0,0 +1,46 @@ +// +// 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 Spezi +import XCTRuntimeAssertions + +private protocol ExampleTypeConstraint: Module {} + +private final class ExampleDependentModule: ExampleTypeConstraint {} + +@resultBuilder +private enum ExampleDependencyBuilder: DependencyCollectionBuilder { + /// An auto-closure expression, providing the default dependency value, building the ``DependencyCollection``. + static func buildExpression(_ expression: @escaping @autoclosure () -> L) -> DependencyCollection { + DependencyCollection(singleEntry: expression) + } +} + +class ExampleDependencyModule: Module { + @Dependency var dependencies: [any Module] + + + init( + @ExampleDependencyBuilder _ dependencies: () -> DependencyCollection + ) { + self._dependencies = Dependency(using: dependencies()) + } +} + + +final class DependencyBuilderTests: XCTestCase { + func testDependencyBuilder() throws { + let module = ExampleDependencyModule { + ExampleDependentModule() + } + let sortedModules = DependencyManager.resolve([module]) + XCTAssertEqual(sortedModules.count, 2) + _ = try XCTUnwrap(sortedModules[0] as? ExampleDependentModule) + _ = try XCTUnwrap(sortedModules[1] as? ExampleDependencyModule) + } +}