1use chrono::{DateTime, Duration as ChronoDuration, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::time::Duration;
5
6use crate::error::{FlowError, Result};
7
8use super::{ChildOperationReference, WorkflowProgress};
9
10pub type JsonValue = Value;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum RuntimeKind {
17 NativeTs,
19 RustEmbedded,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct RuntimeSpec {
26 pub kind: RuntimeKind,
27 pub entrypoint: String,
28 pub export_name: String,
29}
30
31impl RuntimeSpec {
32 pub fn native_ts(entrypoint: impl Into<String>, export_name: impl Into<String>) -> Self {
33 Self {
34 kind: RuntimeKind::NativeTs,
35 entrypoint: entrypoint.into(),
36 export_name: export_name.into(),
37 }
38 }
39
40 pub fn rust_embedded(entrypoint: impl Into<String>, export_name: impl Into<String>) -> Self {
41 Self {
42 kind: RuntimeKind::RustEmbedded,
43 entrypoint: entrypoint.into(),
44 export_name: export_name.into(),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct WorkflowSpec {
52 pub name: String,
53 pub version: String,
54 pub runtime: RuntimeSpec,
55}
56
57impl WorkflowSpec {
58 pub fn native_ts(
59 name: impl Into<String>,
60 version: impl Into<String>,
61 entrypoint: impl Into<String>,
62 export_name: impl Into<String>,
63 ) -> Self {
64 Self {
65 name: name.into(),
66 version: version.into(),
67 runtime: RuntimeSpec::native_ts(entrypoint, export_name),
68 }
69 }
70
71 pub fn rust_embedded(
72 name: impl Into<String>,
73 version: impl Into<String>,
74 entrypoint: impl Into<String>,
75 export_name: impl Into<String>,
76 ) -> Self {
77 Self {
78 name: name.into(),
79 version: version.into(),
80 runtime: RuntimeSpec::rust_embedded(entrypoint, export_name),
81 }
82 }
83
84 pub fn validate(&self) -> Result<()> {
85 if self.name.trim().is_empty() {
86 return Err(FlowError::InvalidWorkflow(
87 "workflow name must not be empty".to_string(),
88 ));
89 }
90 if self.version.trim().is_empty() {
91 return Err(FlowError::InvalidWorkflow(
92 "workflow version must not be empty".to_string(),
93 ));
94 }
95 if self.runtime.entrypoint.trim().is_empty() {
96 return Err(FlowError::InvalidWorkflow(
97 "runtime entrypoint must not be empty".to_string(),
98 ));
99 }
100 if self.runtime.export_name.trim().is_empty() {
101 return Err(FlowError::InvalidWorkflow(
102 "runtime export_name must not be empty".to_string(),
103 ));
104 }
105 Ok(())
106 }
107}
108
109#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
111#[serde(rename_all = "snake_case")]
112pub enum StepFailureAction {
113 #[default]
115 FailRun,
116 ContinueWorkflow,
119}
120
121impl StepFailureAction {
122 pub fn is_fail_run(&self) -> bool {
123 matches!(self, Self::FailRun)
124 }
125}
126
127#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
129pub struct RetryPolicy {
130 pub max_attempts: u32,
131 pub delay_ms: u64,
132 #[serde(default, skip_serializing_if = "StepFailureAction::is_fail_run")]
133 pub on_exhausted: StepFailureAction,
134}
135
136impl RetryPolicy {
137 pub fn none() -> Self {
138 Self {
139 max_attempts: 1,
140 delay_ms: 0,
141 on_exhausted: StepFailureAction::FailRun,
142 }
143 }
144
145 pub fn fixed(max_attempts: u32, delay: Duration) -> Self {
146 Self {
147 max_attempts: max_attempts.max(1),
148 delay_ms: delay.as_millis().min(u128::from(u64::MAX)) as u64,
149 on_exhausted: StepFailureAction::FailRun,
150 }
151 }
152
153 pub fn with_failure_action(mut self, action: StepFailureAction) -> Self {
154 self.on_exhausted = action;
155 self
156 }
157
158 pub fn continue_workflow_on_failure(self) -> Self {
159 self.with_failure_action(StepFailureAction::ContinueWorkflow)
160 }
161
162 pub(crate) fn retry_after(self, now: DateTime<Utc>) -> Result<Option<DateTime<Utc>>> {
163 if self.delay_ms == 0 {
164 return Ok(None);
165 }
166 let delay_ms = i64::try_from(self.delay_ms).map_err(|_| self.invalid_delay_error())?;
167 let delay =
168 ChronoDuration::try_milliseconds(delay_ms).ok_or_else(|| self.invalid_delay_error())?;
169 now.checked_add_signed(delay)
170 .map(Some)
171 .ok_or_else(|| self.invalid_delay_error())
172 }
173
174 fn invalid_delay_error(self) -> FlowError {
175 FlowError::InvalidTransition(format!(
176 "retry delay {}ms cannot be represented as a UTC deadline",
177 self.delay_ms
178 ))
179 }
180}
181
182impl Default for RetryPolicy {
183 fn default() -> Self {
184 Self {
185 max_attempts: 3,
186 delay_ms: 0,
187 on_exhausted: StepFailureAction::FailRun,
188 }
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
194#[serde(tag = "type", rename_all = "snake_case")]
195pub enum RuntimeCommand {
196 Complete {
197 output: JsonValue,
198 },
199 Fail {
200 error: String,
201 },
202 Cancel,
204 Timeout {
206 deadline: DateTime<Utc>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 reason: Option<String>,
209 },
210 RecordProgress {
212 progress: WorkflowProgress,
213 },
214 LinkChildOperation {
216 child: ChildOperationReference,
217 },
218 ScheduleStep {
219 step_id: String,
220 step_name: String,
221 input: JsonValue,
222 #[serde(default)]
223 retry: RetryPolicy,
224 },
225 ScheduleSteps {
226 steps: Vec<StepCommand>,
227 },
228 WaitUntil {
229 wait_id: String,
230 resume_at: DateTime<Utc>,
231 },
232 CreateHook {
233 hook_id: String,
234 token: String,
235 #[serde(default)]
236 metadata: JsonValue,
237 },
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
242pub struct StepCommand {
243 pub step_id: String,
244 pub step_name: String,
245 pub input: JsonValue,
246 #[serde(default)]
247 pub retry: RetryPolicy,
248}
249
250impl StepCommand {
251 pub fn new(step_id: impl Into<String>, step_name: impl Into<String>, input: JsonValue) -> Self {
252 Self {
253 step_id: step_id.into(),
254 step_name: step_name.into(),
255 input,
256 retry: RetryPolicy::default(),
257 }
258 }
259
260 pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
261 self.retry = retry;
262 self
263 }
264}
265
266impl RuntimeCommand {
267 pub fn schedule_step(
268 step_id: impl Into<String>,
269 step_name: impl Into<String>,
270 input: JsonValue,
271 ) -> Self {
272 Self::ScheduleStep {
273 step_id: step_id.into(),
274 step_name: step_name.into(),
275 input,
276 retry: RetryPolicy::default(),
277 }
278 }
279
280 pub fn schedule_steps(steps: Vec<StepCommand>) -> Self {
281 Self::ScheduleSteps { steps }
282 }
283}