Skip to main content

dex_sdk/
step.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use std::any::TypeId;
10use std::marker::PhantomData;
11
12use dex_protocol::dex::Value as ProtoValue;
13
14use crate::channel::ChannelGuard;
15use crate::step_options::ErasedStepOptions;
16use crate::value_mapper;
17use crate::{Context, HandlerResult, SdkResult, StepOptions, Value, Wait, short_type_name};
18
19/// Defines one durable state transition in a Flow.
20///
21/// Dex calls [`Self::wait_for`] to obtain durable conditions, then calls [`Self::execute`] after the
22/// wait is satisfied. The default `wait_for` skips immediately, making execute-only Steps concise.
23/// Handler code may read and stage persistence changes through [`Context`].
24///
25/// # Examples
26///
27/// ```
28/// use dex_sdk::{Context, HandlerResult, Step, StepDecision};
29///
30/// struct CompleteOrder;
31///
32/// impl Step for CompleteOrder {
33///     type Input = String;
34///
35///     fn execute(
36///         &self,
37///         _context: &mut Context,
38///         order_id: Self::Input,
39///     ) -> HandlerResult<StepDecision> {
40///         Ok(StepDecision::graceful_complete(order_id))
41///     }
42/// }
43/// ```
44pub trait Step: Send + Sync + 'static {
45    /// Value decoded for both `wait_for` and `execute`.
46    type Input: Value;
47
48    /// Produces the next movement or terminal Flow decision.
49    ///
50    /// `context` represents one isolated invocation and `input` is a newly decoded owned value.
51    /// Returning [`HandlerError`](crate::HandlerError) activates the configured execute retry and
52    /// failure behavior.
53    fn execute(&self, context: &mut Context, input: Self::Input) -> HandlerResult<StepDecision>;
54
55    /// Returns the durable conditions Dex must satisfy before `execute`.
56    ///
57    /// The default returns [`Wait::skip_immediately`]. Returning an error activates the configured
58    /// `wait_for` retry and failure behavior.
59    fn wait_for(&self, _context: &mut Context, _input: Self::Input) -> HandlerResult<Wait> {
60        Ok(Wait::skip_immediately())
61    }
62
63    /// Returns the stable Step type sent to Dex.
64    ///
65    /// The default is the final Rust type-name segment.
66    fn step_type(&self) -> &'static str {
67        short_type_name::<Self>()
68    }
69
70    /// Returns timeouts, retries, locks, durability, and failure behavior for this Step.
71    ///
72    /// The default preserves server defaults and fails the Flow when `wait_for` exhausts retries.
73    fn options(&self) -> StepOptions<Self::Input> {
74        StepOptions::new()
75    }
76}
77
78/// Collects a Flow's starting Step and remaining Step definitions.
79///
80/// The lifetime ties registered definitions to Step values owned by the Flow. A list may contain at
81/// most one starting Step; [`Self::and`] adds non-starting Steps with arbitrary input types.
82pub struct StepList<'a, StartInput> {
83    definitions: Vec<StepDefinition<'a>>,
84    marker: PhantomData<fn(StartInput)>,
85}
86
87impl<'a, StartInput: Value> StepList<'a, StartInput> {
88    /// Creates a list without a starting Step.
89    pub fn empty() -> Self {
90        Self {
91            definitions: Vec::new(),
92            marker: PhantomData,
93        }
94    }
95
96    /// Creates a list whose starting Step accepts the Flow's start input.
97    pub fn start<StartStep>(step: &'a StartStep) -> Self
98    where
99        StartStep: Step<Input = StartInput>,
100    {
101        Self {
102            definitions: vec![StepDefinition::new(step, true)],
103            marker: PhantomData,
104        }
105    }
106
107    /// Adds a non-starting Step and returns the updated list.
108    pub fn and<OtherStep>(mut self, step: &'a OtherStep) -> Self
109    where
110        OtherStep: Step,
111    {
112        self.definitions.push(StepDefinition::new(step, false));
113        self
114    }
115
116    pub(crate) fn into_definitions(self) -> Vec<RegisteredStep> {
117        self.definitions
118            .into_iter()
119            .map(|definition| RegisteredStep {
120                name: definition.name,
121                starting: definition.starting,
122                input_type: definition.input_type,
123            })
124            .collect()
125    }
126
127    pub(crate) fn find(&self, name: &str) -> Option<&dyn ErasedStep> {
128        self.definitions
129            .iter()
130            .find(|definition| definition.name == name)
131            .map(|definition| definition.handler)
132    }
133}
134
135impl<StartInput: Value> Default for StepList<'_, StartInput> {
136    fn default() -> Self {
137        Self::empty()
138    }
139}
140
141/// Targets a Step with encoded input and an optional per-movement options override.
142///
143/// Create movements with [`Self::to`] or [`Self::to_with_options`], then pass them to
144/// [`StepDecision::go_to_many`], [`crate::RpcResult::then`], or conditional completion.
145pub struct StepMovement {
146    pub(crate) target_step_type: &'static str,
147    input: Box<dyn ErasedValue>,
148    pub(crate) options_override: Option<ErasedStepOptions>,
149}
150
151impl StepMovement {
152    /// Targets `step` with typed `input` and the Step's registered options.
153    pub fn to<TargetStep>(step: &TargetStep, input: TargetStep::Input) -> Self
154    where
155        TargetStep: Step,
156    {
157        Self {
158            target_step_type: step.step_type(),
159            input: Box::new(TypedValue(input)),
160            options_override: None,
161        }
162    }
163
164    /// Targets `step` with typed input and options used only for this movement.
165    pub fn to_with_options<TargetStep>(
166        step: &TargetStep,
167        input: TargetStep::Input,
168        options: StepOptions<TargetStep::Input>,
169    ) -> Self
170    where
171        TargetStep: Step,
172    {
173        Self {
174            target_step_type: step.step_type(),
175            input: Box::new(TypedValue(input)),
176            options_override: Some(options.into()),
177        }
178    }
179
180    pub(crate) fn encode_input(&self) -> SdkResult<ProtoValue> {
181        self.input.encode()
182    }
183}
184
185/// Describes the durable outcome of one successful Step execution.
186///
187/// A decision moves to one or more Steps, completes or fails the Flow, waits for Channels to empty,
188/// or deliberately leaves the Flow at a dead end.
189pub struct StepDecision {
190    pub(crate) kind: StepDecisionKind,
191    pub(crate) cancel_step_types: Vec<&'static str>,
192    pub(crate) cancel_sibling_step_types: Vec<&'static str>,
193}
194
195pub(crate) enum StepDecisionKind {
196    Next(Vec<StepMovement>),
197    GracefulComplete(Box<dyn ErasedValue>),
198    ForceComplete(Box<dyn ErasedValue>),
199    ForceCompleteIfChannelsEmpty {
200        output: Box<dyn ErasedValue>,
201        fallback: Box<StepMovement>,
202        channels: Vec<ChannelGuard>,
203    },
204    ForceFail(String),
205    DeadEnd,
206}
207
208impl StepDecision {
209    /// Moves to one typed target Step.
210    pub fn go_to<TargetStep>(step: &TargetStep, input: TargetStep::Input) -> Self
211    where
212        TargetStep: Step,
213    {
214        Self::go_to_many([StepMovement::to(step, input)])
215    }
216
217    /// Moves to all targets, enabling concurrent active Steps.
218    pub fn go_to_many(movements: impl IntoIterator<Item = StepMovement>) -> Self {
219        Self {
220            kind: StepDecisionKind::Next(movements.into_iter().collect()),
221            cancel_step_types: Vec::new(),
222            cancel_sibling_step_types: Vec::new(),
223        }
224    }
225
226    /// Completes only after all other active Steps finish, returning `output`.
227    pub fn graceful_complete<Output: Value>(output: Output) -> Self {
228        Self {
229            kind: StepDecisionKind::GracefulComplete(Box::new(TypedValue(output))),
230            cancel_step_types: Vec::new(),
231            cancel_sibling_step_types: Vec::new(),
232        }
233    }
234
235    /// Completes immediately, abandoning other active Steps and returning `output`.
236    pub fn force_complete<Output: Value>(output: Output) -> Self {
237        Self {
238            kind: StepDecisionKind::ForceComplete(Box::new(TypedValue(output))),
239            cancel_step_types: Vec::new(),
240            cancel_sibling_step_types: Vec::new(),
241        }
242    }
243
244    /// Completes with `output` when every guard is empty; otherwise follows `fallback`.
245    pub fn force_complete_if_channels_empty<Output: Value>(
246        output: Output,
247        fallback: StepMovement,
248        channels: impl IntoIterator<Item = ChannelGuard>,
249    ) -> Self {
250        Self {
251            kind: StepDecisionKind::ForceCompleteIfChannelsEmpty {
252                output: Box::new(TypedValue(output)),
253                fallback: Box::new(fallback),
254                channels: channels.into_iter().collect(),
255            },
256            cancel_step_types: Vec::new(),
257            cancel_sibling_step_types: Vec::new(),
258        }
259    }
260
261    /// Fails the Flow immediately with an application-provided reason.
262    pub fn force_fail(reason: impl Into<String>) -> Self {
263        Self {
264            kind: StepDecisionKind::ForceFail(reason.into()),
265            cancel_step_types: Vec::new(),
266            cancel_sibling_step_types: Vec::new(),
267        }
268    }
269
270    /// Leaves the Flow running with no next Step.
271    ///
272    /// Use this only when an RPC or external operation will resume the Flow later.
273    pub fn dead_end() -> Self {
274        Self {
275            kind: StepDecisionKind::DeadEnd,
276            cancel_step_types: Vec::new(),
277            cancel_sibling_step_types: Vec::new(),
278        }
279    }
280
281    /// Selects every queued or active execution of one registered Step type.
282    ///
283    /// Dex resolves one snapshot after the current `execute` succeeds. Finished,
284    /// already-canceled, and absent executions are no-ops. Steps scheduled by this decision
285    /// are excluded. Call repeatedly to select multiple types; Flow-wide selection wins.
286    pub fn cancel_step<SelectedStep>(mut self, step: &SelectedStep) -> Self
287    where
288        SelectedStep: Step,
289    {
290        let step_type = step.step_type();
291        if !self.cancel_step_types.contains(&step_type) {
292            self.cancel_step_types.push(step_type);
293        }
294        self.cancel_sibling_step_types
295            .retain(|selected| *selected != step_type);
296        self
297    }
298
299    /// Selects same-source executions of one registered Step type.
300    ///
301    /// A sibling has the same [`Context::from_step_execution_id`](crate::Context::from_step_execution_id)
302    /// as the execution returning this decision. Snapshot and no-op behavior match
303    /// [`Self::cancel_step`]. Call repeatedly to select multiple Step types.
304    pub fn cancel_sibling_step<SelectedStep>(mut self, step: &SelectedStep) -> Self
305    where
306        SelectedStep: Step,
307    {
308        let step_type = step.step_type();
309        if !self.cancel_step_types.contains(&step_type)
310            && !self.cancel_sibling_step_types.contains(&step_type)
311        {
312            self.cancel_sibling_step_types.push(step_type);
313        }
314        self
315    }
316}
317
318#[derive(Clone)]
319pub(crate) struct RegisteredStep {
320    pub(crate) name: &'static str,
321    pub(crate) starting: bool,
322    pub(crate) input_type: TypeId,
323}
324
325struct StepDefinition<'a> {
326    name: &'static str,
327    starting: bool,
328    handler: &'a dyn ErasedStep,
329    input_type: TypeId,
330}
331
332impl<'a> StepDefinition<'a> {
333    fn new<SomeStep>(step: &'a SomeStep, starting: bool) -> Self
334    where
335        SomeStep: Step,
336    {
337        let name = step.step_type();
338        Self {
339            name,
340            starting,
341            handler: step,
342            input_type: TypeId::of::<SomeStep::Input>(),
343        }
344    }
345}
346
347pub(crate) trait ErasedStep: Send + Sync {
348    fn wait_for(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<Wait>;
349    fn execute(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<StepDecision>;
350    fn options(&self) -> ErasedStepOptions;
351}
352
353impl<SomeStep> ErasedStep for SomeStep
354where
355    SomeStep: Step,
356{
357    fn wait_for(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<Wait> {
358        Step::wait_for(self, context, value_mapper::decode_handler(input)?)
359    }
360
361    fn execute(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<StepDecision> {
362        Step::execute(self, context, value_mapper::decode_handler(input)?)
363    }
364
365    fn options(&self) -> ErasedStepOptions {
366        Step::options(self).into()
367    }
368}
369
370pub(crate) trait ErasedValue: Send + Sync {
371    fn encode(&self) -> SdkResult<ProtoValue>;
372}
373
374pub(crate) struct TypedValue<T>(pub(crate) T);
375
376impl<T: Value> ErasedValue for TypedValue<T> {
377    fn encode(&self) -> SdkResult<ProtoValue> {
378        value_mapper::encode(&self.0)
379    }
380}