Coverage for src / ocarina / dsl / testing_with_railway / chain_actions.py: 38.89%
16 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
1"""Chain actions operator for composing Railway action sequences.
3Provides utilities for composing multiple Railway actions into a single
4lazy execution unit, solving the "parenthesis hell" problem.
6Solution:
7 >>> # Flat, readable syntax
8 >>> runner = chain_actions(
9 ... action1.failure(h1).success(h2),
10 ... action2.failure(h3).success(h4),
11 ... action3.failure(h5).success(h6)
12 ... )
13 >>> chain = runner.run() # Execute when ready
15Benefits:
16- Lazy evaluation: Build chain without executing
17- Flat syntax: No deep nesting
18- Automatic short-circuiting: Stops on first failure
19- Composability: ChainRunner is a value
21Example:
22 >>> # Test usage
23 ... def test_scenario(driver, logger):
24 ... return [
25 ... chain_actions(
26 ... act(page, step1).failure(log_error).success(log_success),
27 ... act(page, step2).failure(log_error).success(log_success)
28 ... )
29 ... ]
31"""
33from functools import reduce
34from typing import TYPE_CHECKING, final
36if TYPE_CHECKING:
37 from ocarina.custom_types.thunk import Thunk
38 from ocarina.dsl.testing_with_railway.internals.action_chain import (
39 ActionChain,
40 ActionSuccess,
41 )
44@final
45class ChainRunner[T]:
46 """Lazy execution wrapper for Railway action chains.
48 Encapsulates a sequence of Railway actions that have been composed
49 but not yet executed. Stores a thunk that executes the chain when
50 run() is called.
52 Example:
53 >>> runner = chain_actions(action1, action2)
54 >>> chain = runner.run() # Execute when ready
55 >>> if chain.has_failed():
56 ... logger.error("Chain failed")
58 """
60 def __init__(self, *, thunk: Thunk[ActionChain[T]]) -> None:
61 """Initialize with lazy computation."""
62 self._thunk = thunk
64 def run(self) -> ActionChain[T]:
65 """Execute the lazy action chain and return result.
67 Returns:
68 ActionChain[T] with final state after execution.
70 Example:
71 >>> chain = runner.run()
72 >>> if chain.is_ok():
73 ... logger.success("All succeeded")
75 """
76 return self._thunk()
79def chain_actions[T](
80 first: ActionSuccess[T], *rest: ActionSuccess[T]
81) -> ChainRunner[T]:
82 """Compose multiple configured actions into lazy execution chain.
84 Takes ActionSuccess instances (with both handlers configured) and
85 composes them into a ChainRunner that executes sequentially with
86 automatic short-circuiting on failure.
88 Solves "parenthesis hell" by flattening nested .then().execute() chains.
90 Execution:
91 1. Execute first action with handlers
92 2. If failed: short-circuit, return failed chain
93 3. If succeeded: chain next action with .then()
94 4. Repeat for all actions
95 5. Return final chain state
97 Args:
98 first: First action (with handlers configured).
99 *rest: Additional actions in sequence.
101 Returns:
102 ChainRunner[T] that lazily executes when run() is called.
104 Example:
105 >>> # With short-circuit
106 ... runner = chain_actions(
107 ... failing_action.failure(h1).success(h2),
108 ... never_runs.failure(h3).success(h4) # Skipped
109 ... )
110 >>> chain = runner.run()
111 >>> assert chain.has_failed()
113 Example:
114 >>> runner = chain_actions(
115 ... action1.failure(h1).success(h2),
116 ... action2.failure(h3).success(h4)
117 ... )
118 >>> chain = runner.run()
120 """
122 def thunk() -> ActionChain[T]:
123 """Lazy computation executing the action sequence."""
125 def reducer(chain: ActionChain[T], step: ActionSuccess[T]) -> ActionChain[T]:
126 """Fold function chaining actions with short-circuit logic."""
127 if chain.has_failed():
128 return chain
130 return (
131 chain.then(step.__action__)
132 .failure(step.__failure_handler__)
133 .success(step.__success_handler__)
134 .execute()
135 )
137 return reduce(reducer, rest, first.execute())
139 return ChainRunner(thunk=thunk)