Skip to main content

changeset_saga/
builder.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3
4use crate::erased::{ErasedStep, StepWrapper};
5use crate::saga::Saga;
6use crate::step::SagaStep;
7
8/// Marker type for a builder with no steps.
9pub struct Empty;
10
11/// Marker type for a builder with at least one step.
12pub struct HasSteps<LastOutput>(PhantomData<LastOutput>);
13
14/// Type-state builder for constructing type-safe sagas.
15///
16/// The builder enforces at compile-time that:
17/// - Each step's input type matches the previous step's output type
18/// - The saga's input type matches the first step's input
19/// - The saga's output type matches the last step's output
20///
21/// # Compile-time Type Safety
22///
23/// The builder ensures type safety at compile time. Mismatched types will not compile:
24///
25/// ```compile_fail
26/// use changeset_saga::{SagaBuilder, SagaStep};
27///
28/// struct StepA;
29/// impl SagaStep for StepA {
30///     type Input = i32;
31///     type Output = String;  // Outputs String
32///     type Context = ();
33///     type Error = ();
34///     fn name(&self) -> &'static str { "a" }
35///     fn execute(&self, _: &(), input: i32) -> Result<String, ()> {
36///         Ok(input.to_string())
37///     }
38/// }
39///
40/// struct StepB;
41/// impl SagaStep for StepB {
42///     type Input = i32;  // Expects i32, not String!
43///     type Output = i32;
44///     type Context = ();
45///     type Error = ();
46///     fn name(&self) -> &'static str { "b" }
47///     fn execute(&self, _: &(), input: i32) -> Result<i32, ()> {
48///         Ok(input * 2)
49///     }
50/// }
51///
52/// // This should fail: StepB expects i32 but StepA outputs String
53/// let saga = SagaBuilder::new()
54///     .first_step(StepA)
55///     .then(StepB)  // Compile error here!
56///     .build();
57/// ```
58///
59/// An empty saga (without calling `first_step()`) cannot be built:
60///
61/// ```compile_fail
62/// use changeset_saga::SagaBuilder;
63///
64/// // Cannot build an empty saga - `build()` is only available after `first_step()`
65/// let saga = SagaBuilder::<(), (), (), ()>::new().build();
66/// ```
67pub struct SagaBuilder<Input, Output, Ctx, Err, State> {
68    steps: Vec<Box<dyn ErasedStep<Ctx, Err>>>,
69    _phantom: PhantomData<(Input, Output, State)>,
70}
71
72impl<Ctx, Err> SagaBuilder<(), (), Ctx, Err, Empty> {
73    /// Create a new saga builder in the empty state.
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            steps: Vec::new(),
78            _phantom: PhantomData,
79        }
80    }
81
82    /// Add the first step to the saga.
83    ///
84    /// This establishes the saga's input type from the step's input type.
85    #[must_use]
86    pub fn first_step<S>(
87        self,
88        step: S,
89    ) -> SagaBuilder<S::Input, S::Output, Ctx, Err, HasSteps<S::Output>>
90    where
91        S: SagaStep<Context = Ctx, Error = Err> + 'static,
92    {
93        let mut steps = self.steps;
94        steps.push(Box::new(StepWrapper::new(step)));
95        SagaBuilder {
96            steps,
97            _phantom: PhantomData,
98        }
99    }
100}
101
102impl<Ctx, Err> Default for SagaBuilder<(), (), Ctx, Err, Empty> {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl<Input, CurrentOutput, Ctx, Err>
109    SagaBuilder<Input, CurrentOutput, Ctx, Err, HasSteps<CurrentOutput>>
110{
111    /// Add another step to the saga.
112    ///
113    /// The step's input type must match the current output type.
114    #[must_use]
115    pub fn then<S>(self, step: S) -> SagaBuilder<Input, S::Output, Ctx, Err, HasSteps<S::Output>>
116    where
117        S: SagaStep<Input = CurrentOutput, Context = Ctx, Error = Err> + 'static,
118    {
119        let mut steps = self.steps;
120        steps.push(Box::new(StepWrapper::new(step)));
121        SagaBuilder {
122            steps,
123            _phantom: PhantomData,
124        }
125    }
126
127    /// Build the saga from the accumulated steps.
128    #[must_use]
129    pub fn build(self) -> Saga<Input, CurrentOutput, Ctx, Err>
130    where
131        Input: Clone + Send + 'static,
132        CurrentOutput: Send + 'static,
133        Err: Debug,
134    {
135        Saga::from_steps(self.steps)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    struct TestContext;
144
145    #[derive(Debug, PartialEq)]
146    struct TestError(String);
147
148    struct IntToString;
149
150    impl SagaStep for IntToString {
151        type Input = i32;
152        type Output = String;
153        type Context = TestContext;
154        type Error = TestError;
155
156        fn name(&self) -> &'static str {
157            "int_to_string"
158        }
159
160        fn execute(
161            &self,
162            _ctx: &Self::Context,
163            input: Self::Input,
164        ) -> Result<Self::Output, Self::Error> {
165            Ok(input.to_string())
166        }
167    }
168
169    struct StringToLen;
170
171    impl SagaStep for StringToLen {
172        type Input = String;
173        type Output = usize;
174        type Context = TestContext;
175        type Error = TestError;
176
177        fn name(&self) -> &'static str {
178            "string_to_len"
179        }
180
181        fn execute(
182            &self,
183            _ctx: &Self::Context,
184            input: Self::Input,
185        ) -> Result<Self::Output, Self::Error> {
186            Ok(input.len())
187        }
188    }
189
190    struct DoubleInt;
191
192    impl SagaStep for DoubleInt {
193        type Input = i32;
194        type Output = i32;
195        type Context = TestContext;
196        type Error = TestError;
197
198        fn name(&self) -> &'static str {
199            "double_int"
200        }
201
202        fn execute(
203            &self,
204            _ctx: &Self::Context,
205            input: Self::Input,
206        ) -> Result<Self::Output, Self::Error> {
207            Ok(input * 2)
208        }
209    }
210
211    #[test]
212    fn builder_creates_single_step_saga() {
213        let _saga: Saga<i32, String, TestContext, TestError> =
214            SagaBuilder::new().first_step(IntToString).build();
215    }
216
217    #[test]
218    fn builder_chains_steps_with_matching_types() {
219        let _saga: Saga<i32, usize, TestContext, TestError> = SagaBuilder::new()
220            .first_step(IntToString)
221            .then(StringToLen)
222            .build();
223    }
224
225    #[test]
226    fn builder_allows_multiple_steps_with_same_type() {
227        let _saga: Saga<i32, i32, TestContext, TestError> = SagaBuilder::new()
228            .first_step(DoubleInt)
229            .then(DoubleInt)
230            .then(DoubleInt)
231            .build();
232    }
233}