diff --git a/.github/workflows/genesis.yml b/.github/workflows/genesis.yml index 41a60d178..27e7f647f 100644 --- a/.github/workflows/genesis.yml +++ b/.github/workflows/genesis.yml @@ -20,7 +20,9 @@ jobs: steps: - name: Install dependencies run: | - brew install mint xcodegen needle mockolo + brew install mint xcodegen needle + mint install uber/mockolo@2.5.0 + sudo ln -sf ~/.mint/bin/mockolo /usr/local/bin/mockolo mint install yonaskolb/genesis - name: Checkout source uses: actions/checkout@v6 diff --git a/Sources/Nodes/Core/AbstractBuilder.swift b/Sources/Nodes/Core/AbstractBuilder.swift index fbe7f0692..1a6f5da99 100644 --- a/Sources/Nodes/Core/AbstractBuilder.swift +++ b/Sources/Nodes/Core/AbstractBuilder.swift @@ -91,6 +91,50 @@ open class AbstractBuilder BuildType { + preconditionFailure("Method in abstract base class must be overridden") + } + // swiftlint:enable async_without_await unavailable_function unused_parameter + + /// Async variant of ``build(_:_:)``. Creates a `ComponentType` instance and passes it to the + /// async ``build(component:dynamicBuildDependency:)`` override. + /// + /// - Parameters: + /// - dynamicBuildDependency: The `DynamicBuildDependencyType` instance. + /// - dynamicComponentDependency: The `DynamicComponentDependencyType` instance. + /// + /// - Returns: A `BuildType` instance (`Flow` object). + public final func build( + _ dynamicBuildDependency: DynamicBuildDependencyType, + _ dynamicComponentDependency: DynamicComponentDependencyType + ) async -> BuildType { + let component: ComponentType = componentFactory(dynamicComponentDependency) + let newComponent: AnyObject = component as AnyObject + assert(newComponent !== lastComponent, "Factory must produce a new component each time it is called") + lastComponent = newComponent + return await build(component: component, dynamicBuildDependency: dynamicBuildDependency) + } } // swiftlint:enable period_spacing diff --git a/Sources/Nodes/Core/Config/Plugin.swift b/Sources/Nodes/Core/Config/Plugin.swift index 32e3e4bad..31eaaa0a7 100644 --- a/Sources/Nodes/Core/Config/Plugin.swift +++ b/Sources/Nodes/Core/Config/Plugin.swift @@ -94,6 +94,45 @@ open class Plugin { return build(component: component) } + // MARK: - Async + + // swiftlint:disable async_without_await unavailable_function unused_parameter + /// Async variant of ``build(component:)``. + /// + /// Override this instead of the synchronous version when the plugin needs to + /// `await` async dependency resolution. + /// + /// - Important: This abstract method must be overridden in subclasses that use async building. + /// This method should never be called directly. + /// The plugin calls this method internally. + /// + /// - Parameter component: The `ComponentType` instance. + /// + /// - Returns: A `BuildType` instance. + open func build(component: ComponentType) async -> BuildType { + preconditionFailure("Method in abstract base class must be overridden") + } + // swiftlint:enable async_without_await unavailable_function unused_parameter + + /// Async variant of ``create(state:)``. Returns a `BuildType` instance when enabled, otherwise `nil`. + /// + /// - Parameter state: The `StateType` instance. + /// + /// - Returns: An optional `BuildType` instance. + public func create(state: StateType) async -> BuildType? { + let component: ComponentType = makeComponent() + guard isEnabled(component: component, state: state) + else { return nil } + return await build(component: component) + } + + /// Async variant of ``override()``. Returns a `BuildType` instance ignoring whether the plugin is enabled. + /// + /// - Returns: A `BuildType` instance. + public func override() async -> BuildType { + await build(component: makeComponent()) + } + // MARK: - Access Control: private private func makeComponent() -> ComponentType { @@ -117,4 +156,11 @@ extension Plugin where StateType == Void { public func create() -> BuildType? { create(state: ()) } + + /// Async convenience for plugins whose `StateType` is `Void`. + /// + /// - Returns: An optional `BuildType` instance. + public func create() async -> BuildType? { + await create(state: ()) + } } diff --git a/Tests/NodesTests/CoreTests/AbstractBuilderTests.swift b/Tests/NodesTests/CoreTests/AbstractBuilderTests.swift index d319d4885..3c9e42d81 100644 --- a/Tests/NodesTests/CoreTests/AbstractBuilderTests.swift +++ b/Tests/NodesTests/CoreTests/AbstractBuilderTests.swift @@ -25,12 +25,27 @@ final class AbstractBuilderTests: XCTestCase { } } + private class AsyncTestBuilder: AbstractBuilder { + + // swiftlint:disable:next unused_parameter async_without_await + override func build(component: ComponentType, dynamicBuildDependency: Void) async -> BuildType { + BuildType() + } + } + @MainActor func testBuild() { let builder: TestBuilder = givenBuilder { ComponentType() } expect(builder.build()).to(beAKindOf(BuildType.self)) } + @MainActor + func testAsyncBuild() async { + let builder: AsyncTestBuilder = givenAsyncBuilder { ComponentType() } + let result: BuildType = await builder.buildAsync() + expect(result).to(beAKindOf(BuildType.self)) + } + @MainActor func testAssertions() { let builder: AbstractBuilder = .init { ComponentType() } @@ -44,6 +59,13 @@ final class AbstractBuilderTests: XCTestCase { expect(builder).to(notBeNilAndToDeallocateAfterTest()) return builder } + + @MainActor + private func givenAsyncBuilder(componentFactory: @escaping () -> ComponentType) -> AsyncTestBuilder { + let builder: AsyncTestBuilder = .init(componentFactory: componentFactory) + expect(builder).to(notBeNilAndToDeallocateAfterTest()) + return builder + } } extension AbstractBuilder where DynamicBuildDependencyType == Void, DynamicComponentDependencyType == Void { @@ -51,4 +73,8 @@ extension AbstractBuilder where DynamicBuildDependencyType == Void, DynamicCompo func build() -> BuildType { build((), ()) } + + func buildAsync() async -> BuildType { + await build((), ()) + } } diff --git a/Tests/NodesTests/CoreTests/ConfigTests/PluginTests.swift b/Tests/NodesTests/CoreTests/ConfigTests/PluginTests.swift index fcb82053b..6815f4cbc 100644 --- a/Tests/NodesTests/CoreTests/ConfigTests/PluginTests.swift +++ b/Tests/NodesTests/CoreTests/ConfigTests/PluginTests.swift @@ -32,6 +32,21 @@ final class PluginTests: XCTestCase, TestCaseHelpers { } } + private class AsyncTestPlugin: Plugin { + + var isEnabledOverride: Bool = true + + // swiftlint:disable:next unused_parameter + override func isEnabled(component: ComponentType, state: Void) -> Bool { + isEnabledOverride + } + + // swiftlint:disable:next unused_parameter async_without_await + override func build(component: ComponentType) async -> BuildType { + BuildType() + } + } + @MainActor func testCreate() { let plugin: TestPlugin = .init { ComponentType() } @@ -41,6 +56,17 @@ final class PluginTests: XCTestCase, TestCaseHelpers { expect(plugin.create()) == nil } + @MainActor + func testAsyncCreate() async { + let plugin: AsyncTestPlugin = .init { ComponentType() } + expect(plugin).to(notBeNilAndToDeallocateAfterTest()) + let result: BuildType? = await plugin.create() + expect(result).to(beAKindOf(BuildType.self)) + plugin.isEnabledOverride = false + let nilResult: BuildType? = await plugin.create() + expect(nilResult) == nil + } + @MainActor func testOverride() { let plugin: TestPlugin = .init { ComponentType() } @@ -48,6 +74,14 @@ final class PluginTests: XCTestCase, TestCaseHelpers { expect(plugin.override()).to(beAKindOf(BuildType.self)) } + @MainActor + func testAsyncOverride() async { + let plugin: AsyncTestPlugin = .init { ComponentType() } + expect(plugin).to(notBeNilAndToDeallocateAfterTest()) + let result: BuildType = await plugin.override() + expect(result).to(beAKindOf(BuildType.self)) + } + @MainActor func testAssertions() { let component: ComponentType = .init() @@ -55,4 +89,5 @@ final class PluginTests: XCTestCase, TestCaseHelpers { expect(plugin.isEnabled(component: component, state: ())).to(throwAssertion()) expect(plugin.build(component: component)).to(throwAssertion()) } + }