1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use uuid::Uuid;
10
11use super::{FsmState, RunStatus, TriggerKind};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub struct Run {
29 pub id: Uuid,
31 pub workflow_name: String,
33 pub status: FsmState<RunStatus>,
35 pub trigger: TriggerKind,
37 pub payload: Value,
39 pub error: Option<String>,
41 pub retry_count: u32,
43 pub max_retries: u32,
45 pub cost_usd: Decimal,
47 pub duration_ms: u64,
49 pub created_at: DateTime<Utc>,
51 pub updated_at: DateTime<Utc>,
53 pub started_at: Option<DateTime<Utc>>,
55 pub completed_at: Option<DateTime<Utc>>,
57 pub handler_version: Option<String>,
59 #[serde(default)]
61 pub labels: HashMap<String, String>,
62 #[serde(default)]
64 pub scheduled_at: Option<DateTime<Utc>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct NewRun {
88 pub workflow_name: String,
90 pub trigger: TriggerKind,
92 pub payload: Value,
94 pub max_retries: u32,
96 pub handler_version: Option<String>,
98 #[serde(default)]
100 pub labels: HashMap<String, String>,
101 #[serde(default)]
103 pub scheduled_at: Option<DateTime<Utc>>,
104}
105
106#[derive(Debug, Clone, Default)]
122pub struct RunFilter {
123 pub workflow_name: Option<String>,
125 pub status: Option<RunStatus>,
127 pub created_after: Option<DateTime<Utc>>,
129 pub created_before: Option<DateTime<Utc>>,
131 pub has_steps: Option<bool>,
135 pub labels: Option<HashMap<String, String>>,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct RunUpdate {
153 pub status: Option<RunStatus>,
155 pub error: Option<String>,
157 pub increment_retry: bool,
159 pub cost_usd: Option<Decimal>,
161 pub duration_ms: Option<u64>,
163 pub started_at: Option<DateTime<Utc>>,
165 pub completed_at: Option<DateTime<Utc>>,
167}
168
169#[cfg(test)]
170mod tests {
171 use std::collections::HashMap;
172
173 use super::*;
174 use serde_json::json;
175
176 #[test]
177 fn newrun_serde_roundtrip() {
178 let new_run = NewRun {
179 workflow_name: "deploy".to_string(),
180 trigger: TriggerKind::Manual,
181 payload: json!({"key": "value"}),
182 max_retries: 3,
183 handler_version: Some("1.2.0".to_string()),
184 labels: HashMap::from([("env".to_string(), "prod".to_string())]),
185 scheduled_at: None,
186 };
187
188 let json = serde_json::to_string(&new_run).expect("serialize");
189 let back: NewRun = serde_json::from_str(&json).expect("deserialize");
190 assert_eq!(back.workflow_name, new_run.workflow_name);
191 assert_eq!(back.trigger, new_run.trigger);
192 assert_eq!(back.payload, new_run.payload);
193 assert_eq!(back.max_retries, new_run.max_retries);
194 assert_eq!(back.handler_version, new_run.handler_version);
195 assert_eq!(back.labels, new_run.labels);
196 assert_eq!(back.scheduled_at, new_run.scheduled_at);
197 }
198
199 #[test]
200 fn run_serde_preserves_all_fields() {
201 use crate::entities::FsmState;
202 use chrono::Utc;
203 use uuid::Uuid;
204
205 let now = Utc::now();
206 let run = Run {
207 id: Uuid::now_v7(),
208 workflow_name: "test-wf".to_string(),
209 status: FsmState::new(RunStatus::Running, Uuid::now_v7()),
210 trigger: TriggerKind::Webhook {
211 path: "/hooks/test".to_string(),
212 },
213 payload: json!({"data": 123}),
214 error: Some("test error".to_string()),
215 retry_count: 2,
216 max_retries: 5,
217 cost_usd: Decimal::new(1234, 2),
218 duration_ms: 5000,
219 created_at: now,
220 updated_at: now,
221 started_at: Some(now),
222 completed_at: Some(now),
223 handler_version: Some("2.0.0".to_string()),
224 labels: HashMap::from([
225 ("env".to_string(), "staging".to_string()),
226 ("team".to_string(), "platform".to_string()),
227 ]),
228 scheduled_at: Some(now),
229 };
230
231 let json = serde_json::to_string(&run).expect("serialize");
232 let back: Run = serde_json::from_str(&json).expect("deserialize");
233
234 assert_eq!(back.id, run.id);
235 assert_eq!(back.workflow_name, run.workflow_name);
236 assert_eq!(back.status.state, run.status.state);
237 assert_eq!(back.trigger, run.trigger);
238 assert_eq!(back.payload, run.payload);
239 assert_eq!(back.error, run.error);
240 assert_eq!(back.retry_count, run.retry_count);
241 assert_eq!(back.max_retries, run.max_retries);
242 assert_eq!(back.cost_usd, run.cost_usd);
243 assert_eq!(back.duration_ms, run.duration_ms);
244 assert_eq!(back.started_at, run.started_at);
245 assert_eq!(back.completed_at, run.completed_at);
246 assert_eq!(back.handler_version, run.handler_version);
247 assert_eq!(back.labels, run.labels);
248 assert_eq!(back.scheduled_at, run.scheduled_at);
249 }
250
251 #[test]
252 fn runupdate_serde_roundtrip() {
253 let update = RunUpdate {
254 status: Some(RunStatus::Completed),
255 error: Some("test error".to_string()),
256 increment_retry: true,
257 cost_usd: Some(Decimal::new(5000, 2)),
258 duration_ms: Some(3000),
259 started_at: None,
260 completed_at: None,
261 };
262
263 let json = serde_json::to_string(&update).expect("serialize");
264 let back: RunUpdate = serde_json::from_str(&json).expect("deserialize");
265
266 assert_eq!(back.status, update.status);
267 assert_eq!(back.error, update.error);
268 assert_eq!(back.increment_retry, update.increment_retry);
269 assert_eq!(back.cost_usd, update.cost_usd);
270 assert_eq!(back.duration_ms, update.duration_ms);
271 }
272
273 #[test]
274 fn runfilter_default_is_no_filters() {
275 let filter = RunFilter::default();
276 assert!(filter.workflow_name.is_none());
277 assert!(filter.status.is_none());
278 assert!(filter.created_after.is_none());
279 assert!(filter.created_before.is_none());
280 }
281
282 #[test]
283 fn runfilter_with_multiple_criteria() {
284 let filter = RunFilter {
285 workflow_name: Some("deploy".to_string()),
286 status: Some(RunStatus::Running),
287 ..RunFilter::default()
288 };
289
290 assert_eq!(filter.workflow_name, Some("deploy".to_string()));
291 assert_eq!(filter.status, Some(RunStatus::Running));
292 assert!(filter.created_after.is_none());
293 assert!(filter.created_before.is_none());
294 }
295}