Skip to main content

a3s_flow/
context.rs

1use chrono::{DateTime, Utc};
2use serde::de::DeserializeOwned;
3
4use crate::error::{FlowError, Result};
5use crate::model::{
6    CancellationRequest, ChildOperationReference, FlowEvent, FlowEventEnvelope, HookMetadata,
7    JsonValue, RetryPolicy, RuntimeCommand, StepCommand, WorkflowProgress,
8};
9use crate::runtime::WorkflowInvocation;
10
11/// Replay helper for Rust workflow runtimes.
12///
13/// `WorkflowContext` is a read-only view over a workflow invocation. It provides
14/// deterministic helpers for inspecting persisted history and returning the
15/// next command to the engine.
16pub struct WorkflowContext<'a> {
17    invocation: &'a WorkflowInvocation,
18}
19
20impl<'a> WorkflowContext<'a> {
21    pub fn new(invocation: &'a WorkflowInvocation) -> Self {
22        Self { invocation }
23    }
24
25    pub fn run_id(&self) -> &str {
26        &self.invocation.run_id
27    }
28
29    pub fn input(&self) -> &JsonValue {
30        &self.invocation.input
31    }
32
33    /// Decode the workflow input into a host-defined serde type.
34    pub fn input_as<T>(&self) -> Result<T>
35    where
36        T: DeserializeOwned,
37    {
38        self.invocation.input_as()
39    }
40
41    pub fn history(&self) -> &[FlowEventEnvelope] {
42        &self.invocation.history
43    }
44
45    /// Return the durable cleanup-aware cancellation request, when present.
46    pub fn cancellation_request(&self) -> Option<&CancellationRequest> {
47        self.history()
48            .iter()
49            .find_map(|envelope| match &envelope.event {
50                FlowEvent::RunCancellationRequested { request } => Some(request),
51                _ => None,
52            })
53    }
54
55    /// Return a durable progress update by its idempotency identity.
56    pub fn progress(&self, progress_id: &str) -> Option<&WorkflowProgress> {
57        self.history()
58            .iter()
59            .find_map(|envelope| match &envelope.event {
60                FlowEvent::RunProgressRecorded { progress }
61                    if progress.progress_id == progress_id =>
62                {
63                    Some(progress)
64                }
65                _ => None,
66            })
67    }
68
69    /// Return a durable child-operation reference by its parent-local id.
70    pub fn child_operation(&self, reference_id: &str) -> Option<&ChildOperationReference> {
71        self.history()
72            .iter()
73            .find_map(|envelope| match &envelope.event {
74                FlowEvent::ChildOperationLinked { child } if child.reference_id == reference_id => {
75                    Some(child)
76                }
77                _ => None,
78            })
79    }
80
81    pub fn step_output(&self, step_id: &str) -> Option<&JsonValue> {
82        self.history()
83            .iter()
84            .find_map(|envelope| match &envelope.event {
85                FlowEvent::StepCompleted {
86                    step_id: id,
87                    output,
88                } if id == step_id => Some(output),
89                _ => None,
90            })
91    }
92
93    pub fn step_output_as<T>(&self, step_id: &str) -> Result<Option<T>>
94    where
95        T: DeserializeOwned,
96    {
97        self.step_output(step_id)
98            .cloned()
99            .map(serde_json::from_value)
100            .transpose()
101            .map_err(FlowError::from)
102    }
103
104    pub fn step_completed(&self, step_id: &str) -> bool {
105        self.step_output(step_id).is_some()
106    }
107
108    pub fn step_failed(&self, step_id: &str) -> Option<&str> {
109        self.history()
110            .iter()
111            .rev()
112            .find_map(|envelope| match &envelope.event {
113                FlowEvent::StepFailed {
114                    step_id: id, error, ..
115                } if id == step_id => Some(error.as_str()),
116                _ => None,
117            })
118    }
119
120    pub fn wait_completed(&self, wait_id: &str) -> bool {
121        self.history().iter().any(|envelope| {
122            matches!(
123                &envelope.event,
124                FlowEvent::WaitCompleted { wait_id: id } if id == wait_id
125            )
126        })
127    }
128
129    pub fn hook_payload(&self, hook_id: &str) -> Option<&JsonValue> {
130        self.history()
131            .iter()
132            .find_map(|envelope| match &envelope.event {
133                FlowEvent::HookReceived {
134                    hook_id: id,
135                    payload,
136                } if id == hook_id => Some(payload),
137                _ => None,
138            })
139    }
140
141    pub fn hook_payload_as<T>(&self, hook_id: &str) -> Result<Option<T>>
142    where
143        T: DeserializeOwned,
144    {
145        self.hook_payload(hook_id)
146            .cloned()
147            .map(serde_json::from_value)
148            .transpose()
149            .map_err(FlowError::from)
150    }
151
152    pub fn hook_disposed(&self, hook_id: &str) -> bool {
153        self.history().iter().any(|envelope| {
154            matches!(
155                &envelope.event,
156                FlowEvent::HookDisposed { hook_id: id } if id == hook_id
157            )
158        })
159    }
160
161    pub fn complete(&self, output: JsonValue) -> RuntimeCommand {
162        RuntimeCommand::Complete { output }
163    }
164
165    pub fn fail(&self, error: impl Into<String>) -> RuntimeCommand {
166        RuntimeCommand::Fail {
167            error: error.into(),
168        }
169    }
170
171    /// Finish a previously requested cancellation after cleanup is durable.
172    pub fn cancel(&self) -> RuntimeCommand {
173        RuntimeCommand::Cancel
174    }
175
176    /// Finish a run with a typed timeout outcome.
177    pub fn timeout(&self, deadline: DateTime<Utc>, reason: Option<String>) -> RuntimeCommand {
178        RuntimeCommand::Timeout { deadline, reason }
179    }
180
181    /// Persist an idempotently identified progress update and replay.
182    pub fn record_progress(&self, progress: WorkflowProgress) -> RuntimeCommand {
183        RuntimeCommand::RecordProgress { progress }
184    }
185
186    /// Persist a child-operation reference and replay.
187    pub fn link_child_operation(&self, child: ChildOperationReference) -> RuntimeCommand {
188        RuntimeCommand::LinkChildOperation { child }
189    }
190
191    pub fn schedule_step(
192        &self,
193        step_id: impl Into<String>,
194        step_name: impl Into<String>,
195        input: JsonValue,
196    ) -> RuntimeCommand {
197        RuntimeCommand::schedule_step(step_id, step_name, input)
198    }
199
200    pub fn schedule_step_with_retry(
201        &self,
202        step_id: impl Into<String>,
203        step_name: impl Into<String>,
204        input: JsonValue,
205        retry: RetryPolicy,
206    ) -> RuntimeCommand {
207        RuntimeCommand::ScheduleStep {
208            step_id: step_id.into(),
209            step_name: step_name.into(),
210            input,
211            retry,
212        }
213    }
214
215    pub fn step(
216        &self,
217        step_id: impl Into<String>,
218        step_name: impl Into<String>,
219        input: JsonValue,
220    ) -> StepCommand {
221        StepCommand::new(step_id, step_name, input)
222    }
223
224    pub fn step_with_retry(
225        &self,
226        step_id: impl Into<String>,
227        step_name: impl Into<String>,
228        input: JsonValue,
229        retry: RetryPolicy,
230    ) -> StepCommand {
231        StepCommand::new(step_id, step_name, input).with_retry(retry)
232    }
233
234    pub fn schedule_steps(&self, steps: Vec<StepCommand>) -> RuntimeCommand {
235        RuntimeCommand::schedule_steps(steps)
236    }
237
238    pub fn wait_until(
239        &self,
240        wait_id: impl Into<String>,
241        resume_at: DateTime<Utc>,
242    ) -> RuntimeCommand {
243        RuntimeCommand::WaitUntil {
244            wait_id: wait_id.into(),
245            resume_at,
246        }
247    }
248
249    pub fn create_hook(
250        &self,
251        hook_id: impl Into<String>,
252        token: impl Into<String>,
253        metadata: JsonValue,
254    ) -> RuntimeCommand {
255        RuntimeCommand::CreateHook {
256            hook_id: hook_id.into(),
257            token: token.into(),
258            metadata,
259        }
260    }
261
262    pub fn create_hook_with_metadata(
263        &self,
264        hook_id: impl Into<String>,
265        token: impl Into<String>,
266        metadata: HookMetadata,
267    ) -> Result<RuntimeCommand> {
268        Ok(self.create_hook(hook_id, token, metadata.into_json()?))
269    }
270}