Skip to content

Commit

Permalink
Enforcement of customizable type constraints for DependencyBuilder (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippzagar authored Dec 7, 2023
1 parent 092eabc commit d56ed07
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 36 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
34 changes: 1 addition & 33 deletions Sources/Spezi/Dependencies/DependencyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<M: Module>(_ 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)
})
}
}
24 changes: 24 additions & 0 deletions Sources/Spezi/Dependencies/DependencyCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: SomeTypeConstraint>(_ 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<Dependency: Module>(for type: Dependency.Type = Dependency.self, singleEntry: @escaping (() -> Dependency)) {
self.init(DependencyContext(for: type, defaultValue: singleEntry))
}


func collect(into dependencyManager: DependencyManager) {
Expand Down
56 changes: 56 additions & 0 deletions Sources/Spezi/Dependencies/DependencyCollectionBuilder.swift
Original file line number Diff line number Diff line change
@@ -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<M: Module>(_ 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)
})
}
}
28 changes: 26 additions & 2 deletions Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<WrappedValue>(as value: WrappedValue.Type) -> WrappedValue {
guard let modules = dependencies.retrieveModules() as? WrappedValue else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Spezi/Module/ModuleBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Sources/Spezi/Spezi.docc/Module/Module Dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ class ExampleModule: Module {
### Building Dependencies

- ``DependencyBuilder``
- ``DependencyCollectionBuilder``
- ``DependencyCollection``
46 changes: 46 additions & 0 deletions Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift
Original file line number Diff line number Diff line change
@@ -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<L: ExampleTypeConstraint>(_ 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)
}
}

0 comments on commit d56ed07

Please sign in to comment.