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
83impl<Ctx, Err> Default for SagaBuilder<(), (), Ctx, Err, Empty> {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl<Ctx, Err> SagaBuilder<(), (), Ctx, Err, Empty> {
90    /// Add the first step to the saga.
91    ///
92    /// This establishes the saga's input type from the step's input type.
93    #[must_use]
94    pub fn first_step<S>(
95        self,
96        step: S,
97    ) -> SagaBuilder<S::Input, S::Output, Ctx, Err, HasSteps<S::Output>>
98    where
99        S: SagaStep<Context = Ctx, Error = Err> + 'static,
100    {
101        let mut steps = self.steps;
102        steps.push(Box::new(StepWrapper::new(step)));
103        SagaBuilder {
104            steps,
105            _phantom: PhantomData,
106        }
107    }
108}
109
110impl<Input, CurrentOutput, Ctx, Err>
111    SagaBuilder<Input, CurrentOutput, Ctx, Err, HasSteps<CurrentOutput>>
112{
113    /// Add another step to the saga.
114    ///
115    /// The step's input type must match the current output type.
116    #[must_use]
117    pub fn then<S>(self, step: S) -> SagaBuilder<Input, S::Output, Ctx, Err, HasSteps<S::Output>>
118    where
119        S: SagaStep<Input = CurrentOutput, Context = Ctx, Error = Err> + 'static,
120    {
121        let mut steps = self.steps;
122        steps.push(Box::new(StepWrapper::new(step)));
123        SagaBuilder {
124            steps,
125            _phantom: PhantomData,
126        }
127    }
128
129    /// Build the saga from the accumulated steps.
130    #[must_use]
131    pub fn build(self) -> Saga<Input, CurrentOutput, Ctx, Err>
132    where
133        Input: Clone + Send + 'static,
134        CurrentOutput: Send + 'static,
135        Err: Debug,
136    {
137        Saga::from_steps(self.steps)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    struct TestContext;
146
147    #[derive(Debug, PartialEq)]
148    struct TestError(String);
149
150    struct IntToString;
151
152    impl SagaStep for IntToString {
153        type Input = i32;
154        type Output = String;
155        type Context = TestContext;
156        type Error = TestError;
157
158        fn name(&self) -> &'static str {
159            "int_to_string"
160        }
161
162        fn execute(
163            &self,
164            _ctx: &Self::Context,
165            input: Self::Input,
166        ) -> Result<Self::Output, Self::Error> {
167            Ok(input.to_string())
168        }
169    }
170
171    struct StringToLen;
172
173    impl SagaStep for StringToLen {
174        type Input = String;
175        type Output = usize;
176        type Context = TestContext;
177        type Error = TestError;
178
179        fn name(&self) -> &'static str {
180            "string_to_len"
181        }
182
183        fn execute(
184            &self,
185            _ctx: &Self::Context,
186            input: Self::Input,
187        ) -> Result<Self::Output, Self::Error> {
188            Ok(input.len())
189        }
190    }
191
192    struct DoubleInt;
193
194    impl SagaStep for DoubleInt {
195        type Input = i32;
196        type Output = i32;
197        type Context = TestContext;
198        type Error = TestError;
199
200        fn name(&self) -> &'static str {
201            "double_int"
202        }
203
204        fn execute(
205            &self,
206            _ctx: &Self::Context,
207            input: Self::Input,
208        ) -> Result<Self::Output, Self::Error> {
209            Ok(input * 2)
210        }
211    }
212
213    #[test]
214    fn builder_creates_single_step_saga() {
215        let _saga: Saga<i32, String, TestContext, TestError> =
216            SagaBuilder::new().first_step(IntToString).build();
217    }
218
219    #[test]
220    fn builder_chains_steps_with_matching_types() {
221        let _saga: Saga<i32, usize, TestContext, TestError> = SagaBuilder::new()
222            .first_step(IntToString)
223            .then(StringToLen)
224            .build();
225    }
226
227    #[test]
228    fn builder_allows_multiple_steps_with_same_type() {
229        let _saga: Saga<i32, i32, TestContext, TestError> = SagaBuilder::new()
230            .first_step(DoubleInt)
231            .then(DoubleInt)
232            .then(DoubleInt)
233            .build();
234    }
235}