Skip to content

[feat]: give AnyWorkflow a Workflow conformance #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 7, 2023
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
33 changes: 27 additions & 6 deletions Workflow/Sources/AnyWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@
public struct AnyWorkflow<Rendering, Output> {
private let storage: AnyStorage

/// The underlying erased workflow instance
public var base: Any { storage.base }

private init(storage: AnyStorage) {
self.storage = storage
}

/// Initializes a new type-erased wrapper for the given workflow.
public init<T: Workflow>(_ workflow: T) where T.Rendering == Rendering, T.Output == Output {
self.init(storage: Storage<T>(
workflow: workflow,
renderingTransform: { $0 },
outputTransform: { $0 }
))
if let workflow = workflow as? AnyWorkflow<Rendering, Output> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to get the compiler to do this without having to perform the runtime check?

self = workflow
} else {
self.init(storage: Storage<T>(
workflow: workflow,
renderingTransform: { $0 },
outputTransform: { $0 }
))
}
}

/// The underlying workflow's implementation type.
Expand All @@ -37,7 +44,17 @@ public struct AnyWorkflow<Rendering, Output> {
}
}

extension AnyWorkflow: AnyWorkflowConvertible {
extension AnyWorkflow: Workflow {
public typealias Output = Output
public typealias State = Void
public typealias Rendering = Rendering

public func render(state: Void, context: RenderContext<AnyWorkflow<Rendering, Output>>) -> Rendering {
storage.render(context: context, key: "") {
AnyWorkflowAction(sendingOutput: $0)
}
}

public func asAnyWorkflow() -> AnyWorkflow<Rendering, Output> {
return self
}
Expand Down Expand Up @@ -84,6 +101,8 @@ extension AnyWorkflow {
///
/// This type is never used directly.
fileprivate class AnyStorage {
var base: Any { fatalError() }

func render<Parent, Action>(context: RenderContext<Parent>, key: String, outputMap: @escaping (Output) -> Action) -> Rendering where Action: WorkflowAction, Action.WorkflowType == Parent {
fatalError()
}
Expand Down Expand Up @@ -115,6 +134,8 @@ extension AnyWorkflow {
self.outputTransform = outputTransform
}

override var base: Any { workflow }

override var workflowType: Any.Type {
return T.self
}
Expand Down
28 changes: 28 additions & 0 deletions Workflow/Tests/AnyWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ public class AnyWorkflowTests: XCTestCase {
}
wait(for: [renderingExpectation, outputExpectation], timeout: 1)
}

func testOnlyWrapsOnce() {
// direct initializer
do {
let base = OnOutputWorkflow()
let wrappedOnce = AnyWorkflow(base)
let wrappedTwice = AnyWorkflow(wrappedOnce)

XCTAssertNotNil(wrappedOnce.base as? OnOutputWorkflow)
XCTAssertNotNil(wrappedTwice.base as? OnOutputWorkflow)
}

// method chaining
do {
let base = OnOutputWorkflow()
let wrappedOnce = base.asAnyWorkflow()
let wrappedTwice = base.asAnyWorkflow().asAnyWorkflow()

XCTAssertNotNil(wrappedOnce.base as? OnOutputWorkflow)
XCTAssertNotNil(wrappedTwice.base as? OnOutputWorkflow)
}
}

func testBaseValue() {
let erased = OnOutputWorkflow().asAnyWorkflow()

XCTAssertNotNil(erased.base as? OnOutputWorkflow)
}
}

/// Has no state or output, simply renders a reversed string
Expand Down
69 changes: 69 additions & 0 deletions WorkflowTesting/Tests/WorkflowRenderTesterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,40 @@ final class WorkflowRenderTesterTests: XCTestCase {
.assertNoOutput()
}

func test_ignoredOutput_opaqueChild() {
OpaqueChildOutputIgnoringWorkflow(
childProvider: {
OutputWorkflow()
.mapRendering { _ in "screen" }
.asAnyWorkflow()
}
)
.renderTester()
.expectWorkflowIgnoringOutput(
type: AnyWorkflow<String, OutputWorkflow.Output>.self,
producingRendering: "test"
)
.render { rendering in
XCTAssertEqual(rendering, "test")
}
}

func test_opaqueChild() {
OpaqueChildWorkflow(
childProvider: {
MockChildWorkflow().asAnyWorkflow()
}
)
.renderTester()
.expectWorkflow(
type: MockChildWorkflow.self,
producingRendering: "test"
)
.render { rendering in
XCTAssertEqual(rendering, "test")
}
}

func test_childWorkflow() {
ParentWorkflow(initialText: "hello")
.renderTester()
Expand Down Expand Up @@ -262,6 +296,41 @@ private struct OutputIgnoringWorkflow: Workflow {
}
}

private struct MockChildWorkflow: Workflow {
typealias State = Void
typealias Rendering = String

func render(state: Void, context: RenderContext<MockChildWorkflow>) -> String {
XCTFail("should never be rendered")
return ""
}
}

private struct OpaqueChildWorkflow: Workflow {
typealias State = Void
typealias Rendering = String

var childProvider: () -> AnyWorkflow<String, Never>

func render(state: Void, context: RenderContext<Self>) -> Rendering {
childProvider()
.rendered(in: context)
}
}

private struct OpaqueChildOutputIgnoringWorkflow: Workflow {
typealias State = Void
typealias Rendering = String

var childProvider: () -> AnyWorkflow<String, OutputWorkflow.Output>

func render(state: Void, context: RenderContext<Self>) -> Rendering {
childProvider()
.ignoringOutput()
.rendered(in: context)
}
}

private struct TestSideEffectKey: Hashable {
let key: String = "Test Side Effect"
}
Expand Down
27 changes: 3 additions & 24 deletions WorkflowUI/Sources/Hosting/WorkflowHostingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public final class WorkflowHostingController<ScreenType, Output>: UIViewControll

private(set) var rootViewController: UIViewController

private let workflowHost: WorkflowHost<RootWorkflow<ScreenType, Output>>
private let workflowHost: WorkflowHost<AnyWorkflow<ScreenType, Output>>

private let (lifetime, token) = Lifetime.make()

Expand All @@ -45,7 +45,7 @@ public final class WorkflowHostingController<ScreenType, Output>: UIViewControll
observers: [WorkflowObserver] = []
) where W.Rendering == ScreenType, W.Output == Output {
self.workflowHost = WorkflowHost(
workflow: RootWorkflow(workflow),
workflow: workflow.asAnyWorkflow(),
observers: observers
)

Expand Down Expand Up @@ -74,7 +74,7 @@ public final class WorkflowHostingController<ScreenType, Output>: UIViewControll

/// Updates the root Workflow in this container.
public func update<W: AnyWorkflowConvertible>(workflow: W) where W.Rendering == ScreenType, W.Output == Output {
workflowHost.update(workflow: RootWorkflow(workflow))
workflowHost.update(workflow: workflow.asAnyWorkflow())
}

public required init?(coder aDecoder: NSCoder) {
Expand Down Expand Up @@ -150,25 +150,4 @@ public final class WorkflowHostingController<ScreenType, Output>: UIViewControll
}
}

/// Wrapper around an AnyWorkflow that allows us to have a concrete
/// WorkflowHost without WorkflowHostingController itself being generic
/// around a Workflow.
fileprivate struct RootWorkflow<Rendering, Output>: Workflow {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer necessary

typealias State = Void
typealias Output = Output
typealias Rendering = Rendering

var wrapped: AnyWorkflow<Rendering, Output>

init<W: AnyWorkflowConvertible>(_ wrapped: W) where W.Rendering == Rendering, W.Output == Output {
self.wrapped = wrapped.asAnyWorkflow()
}

func render(state: State, context: RenderContext<RootWorkflow>) -> Rendering {
return wrapped
.mapOutput { AnyWorkflowAction(sendingOutput: $0) }
.rendered(in: context)
}
}

#endif