1use chrono::{DateTime, Utc};
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4
5use crate::error::{FlowError, Result};
6
7use super::JsonValue;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[non_exhaustive]
16pub struct WorkflowContinuation {
17 pub successor_run_id: String,
19 pub input: JsonValue,
21}
22
23impl WorkflowContinuation {
24 pub fn input_as<T>(&self) -> Result<T>
26 where
27 T: DeserializeOwned,
28 {
29 serde_json::from_value(self.input.clone()).map_err(FlowError::from)
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct CancellationRequest {
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub reason: Option<String>,
40}
41
42impl CancellationRequest {
43 pub fn new(reason: Option<String>) -> Self {
45 Self { reason }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct CancellationRequestSnapshot {
53 pub request: CancellationRequest,
55 pub requested_at: DateTime<Utc>,
57 pub sequence: u64,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63#[non_exhaustive]
64pub struct WorkflowProgress {
65 pub progress_id: String,
67 pub completed: u64,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub total: Option<u64>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub message: Option<String>,
75 #[serde(default, skip_serializing_if = "JsonValue::is_null")]
77 pub details: JsonValue,
78}
79
80impl WorkflowProgress {
81 pub fn new(progress_id: impl Into<String>, completed: u64) -> Self {
83 Self {
84 progress_id: progress_id.into(),
85 completed,
86 total: None,
87 message: None,
88 details: JsonValue::Null,
89 }
90 }
91
92 pub fn with_total(mut self, total: u64) -> Self {
94 self.total = Some(total);
95 self
96 }
97
98 pub fn with_message(mut self, message: impl Into<String>) -> Self {
100 self.message = Some(message.into());
101 self
102 }
103
104 pub fn with_details(mut self, details: JsonValue) -> Self {
106 self.details = details;
107 self
108 }
109
110 pub(crate) fn validate(&self) -> Result<()> {
111 if self.progress_id.trim().is_empty() {
112 return Err(FlowError::InvalidTransition(
113 "workflow progress id must not be empty".to_string(),
114 ));
115 }
116 if self
117 .total
118 .is_some_and(|total| total == 0 || self.completed > total)
119 {
120 return Err(FlowError::InvalidTransition(format!(
121 "workflow progress {} must satisfy completed <= total and total > 0",
122 self.progress_id
123 )));
124 }
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135#[non_exhaustive]
136pub struct ChildOperationReference {
137 pub reference_id: String,
139 pub kind: String,
141 pub operation_id: String,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub flow_run_id: Option<String>,
146 #[serde(default, skip_serializing_if = "JsonValue::is_null")]
148 pub metadata: JsonValue,
149}
150
151impl ChildOperationReference {
152 pub fn new(
154 reference_id: impl Into<String>,
155 kind: impl Into<String>,
156 operation_id: impl Into<String>,
157 ) -> Self {
158 Self {
159 reference_id: reference_id.into(),
160 kind: kind.into(),
161 operation_id: operation_id.into(),
162 flow_run_id: None,
163 metadata: JsonValue::Null,
164 }
165 }
166
167 pub fn with_flow_run_id(mut self, flow_run_id: impl Into<String>) -> Self {
169 self.flow_run_id = Some(flow_run_id.into());
170 self
171 }
172
173 pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
175 self.metadata = metadata;
176 self
177 }
178
179 pub(crate) fn validate(&self) -> Result<()> {
180 if self.reference_id.trim().is_empty() {
181 return Err(FlowError::InvalidTransition(
182 "child operation reference id must not be empty".to_string(),
183 ));
184 }
185 if self.kind.trim().is_empty() {
186 return Err(FlowError::InvalidTransition(format!(
187 "child operation {} kind must not be empty",
188 self.reference_id
189 )));
190 }
191 if self.operation_id.trim().is_empty() {
192 return Err(FlowError::InvalidTransition(format!(
193 "child operation {} operation id must not be empty",
194 self.reference_id
195 )));
196 }
197 if self
198 .flow_run_id
199 .as_deref()
200 .is_some_and(|run_id| run_id.trim().is_empty())
201 {
202 return Err(FlowError::InvalidTransition(format!(
203 "child operation {} Flow run id must not be empty",
204 self.reference_id
205 )));
206 }
207 Ok(())
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[non_exhaustive]
214#[serde(tag = "type", rename_all = "snake_case")]
215pub enum WorkflowTerminalOutcome {
216 Completed {
218 output: JsonValue,
220 },
221 Failed {
223 error: String,
225 },
226 Cancelled {
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 reason: Option<String>,
231 },
232 TimedOut {
234 deadline: DateTime<Utc>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 reason: Option<String>,
239 },
240 RetryExhausted {
242 step_id: String,
244 attempt: u32,
246 error: String,
248 },
249 HostShutdown {
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 reason: Option<String>,
254 },
255 ContinuedAsNew {
257 successor_run_id: String,
259 },
260}