Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/genesis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions Sources/Nodes/Core/AbstractBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,50 @@ open class AbstractBuilder<ComponentType,
lastComponent = newComponent
return build(component: component, dynamicBuildDependency: dynamicBuildDependency)
}

// MARK: - Async

// swiftlint:disable async_without_await unavailable_function unused_parameter
/// Async variant of the factory method ``build(component:dynamicBuildDependency:)``.
///
/// Override this instead of the synchronous version when your builder 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 ``AbstractBuilder`` instance calls this method internally.
///
/// - Parameters:
/// - component: The `ComponentType` instance.
/// - dynamicBuildDependency: The `DynamicBuildDependencyType` instance.
///
/// - Returns: A `BuildType` instance (`Flow` object).
open func build(
component: ComponentType,
dynamicBuildDependency: DynamicBuildDependencyType
) async -> 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
46 changes: 46 additions & 0 deletions Sources/Nodes/Core/Config/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,45 @@ open class Plugin<ComponentType, BuildType, StateType> {
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 {
Expand All @@ -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: ())
}
}
26 changes: 26 additions & 0 deletions Tests/NodesTests/CoreTests/AbstractBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,27 @@ final class AbstractBuilderTests: XCTestCase {
}
}

private class AsyncTestBuilder: AbstractBuilder<ComponentType, BuildType, Void, Void> {

// 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<ComponentType, BuildType, Void, Void> = .init { ComponentType() }
Expand All @@ -44,11 +59,22 @@ 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 {

func build() -> BuildType {
build((), ())
}

func buildAsync() async -> BuildType {
await build((), ())
}
}
35 changes: 35 additions & 0 deletions Tests/NodesTests/CoreTests/ConfigTests/PluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ final class PluginTests: XCTestCase, TestCaseHelpers {
}
}

private class AsyncTestPlugin: Plugin<ComponentType, BuildType, Void> {

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() }
Expand All @@ -41,18 +56,38 @@ 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() }
expect(plugin).to(notBeNilAndToDeallocateAfterTest())
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()
let plugin: Plugin<ComponentType, BuildType, Void> = .init { component }
expect(plugin.isEnabled(component: component, state: ())).to(throwAssertion())
expect(plugin.build(component: component)).to(throwAssertion())
}

}