1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9use super::{FsmState, RunStatus, TriggerKind};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct Run {
27 pub id: Uuid,
29 pub workflow_name: String,
31 pub status: FsmState<RunStatus>,
33 pub trigger: TriggerKind,
35 pub payload: Value,
37 pub error: Option<String>,
39 pub retry_count: u32,
41 pub max_retries: u32,
43 pub cost_usd: Decimal,
45 pub duration_ms: u64,
47 pub created_at: DateTime<Utc>,
49 pub updated_at: DateTime<Utc>,
51 pub started_at: Option<DateTime<Utc>>,
53 pub completed_at: Option<DateTime<Utc>>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NewRun {
74 pub workflow_name: String,
76 pub trigger: TriggerKind,
78 pub payload: Value,
80 pub max_retries: u32,
82}
83
84#[derive(Debug, Clone, Default)]
100pub struct RunFilter {
101 pub workflow_name: Option<String>,
103 pub status: Option<RunStatus>,
105 pub created_after: Option<DateTime<Utc>>,
107 pub created_before: Option<DateTime<Utc>>,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct RunUpdate {
125 pub status: Option<RunStatus>,
127 pub error: Option<String>,
129 pub increment_retry: bool,
131 pub cost_usd: Option<Decimal>,
133 pub duration_ms: Option<u64>,
135 pub started_at: Option<DateTime<Utc>>,
137 pub completed_at: Option<DateTime<Utc>>,
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use serde_json::json;
145
146 #[test]
147 fn newrun_serde_roundtrip() {
148 let new_run = NewRun {
149 workflow_name: "deploy".to_string(),
150 trigger: TriggerKind::Manual,
151 payload: json!({"key": "value"}),
152 max_retries: 3,
153 };
154
155 let json = serde_json::to_string(&new_run).expect("serialize");
156 let back: NewRun = serde_json::from_str(&json).expect("deserialize");
157 assert_eq!(back.workflow_name, new_run.workflow_name);
158 assert_eq!(back.trigger, new_run.trigger);
159 assert_eq!(back.payload, new_run.payload);
160 assert_eq!(back.max_retries, new_run.max_retries);
161 }
162
163 #[test]
164 fn run_serde_preserves_all_fields() {
165 use crate::entities::FsmState;
166 use chrono::Utc;
167 use uuid::Uuid;
168
169 let now = Utc::now();
170 let run = Run {
171 id: Uuid::now_v7(),
172 workflow_name: "test-wf".to_string(),
173 status: FsmState::new(RunStatus::Running, Uuid::now_v7()),
174 trigger: TriggerKind::Webhook {
175 path: "/hooks/test".to_string(),
176 },
177 payload: json!({"data": 123}),
178 error: Some("test error".to_string()),
179 retry_count: 2,
180 max_retries: 5,
181 cost_usd: Decimal::new(1234, 2),
182 duration_ms: 5000,
183 created_at: now,
184 updated_at: now,
185 started_at: Some(now),
186 completed_at: Some(now),
187 };
188
189 let json = serde_json::to_string(&run).expect("serialize");
190 let back: Run = serde_json::from_str(&json).expect("deserialize");
191
192 assert_eq!(back.id, run.id);
193 assert_eq!(back.workflow_name, run.workflow_name);
194 assert_eq!(back.status.state, run.status.state);
195 assert_eq!(back.trigger, run.trigger);
196 assert_eq!(back.payload, run.payload);
197 assert_eq!(back.error, run.error);
198 assert_eq!(back.retry_count, run.retry_count);
199 assert_eq!(back.max_retries, run.max_retries);
200 assert_eq!(back.cost_usd, run.cost_usd);
201 assert_eq!(back.duration_ms, run.duration_ms);
202 assert_eq!(back.started_at, run.started_at);
203 assert_eq!(back.completed_at, run.completed_at);
204 }
205
206 #[test]
207 fn runupdate_serde_roundtrip() {
208 let update = RunUpdate {
209 status: Some(RunStatus::Completed),
210 error: Some("test error".to_string()),
211 increment_retry: true,
212 cost_usd: Some(Decimal::new(5000, 2)),
213 duration_ms: Some(3000),
214 started_at: None,
215 completed_at: None,
216 };
217
218 let json = serde_json::to_string(&update).expect("serialize");
219 let back: RunUpdate = serde_json::from_str(&json).expect("deserialize");
220
221 assert_eq!(back.status, update.status);
222 assert_eq!(back.error, update.error);
223 assert_eq!(back.increment_retry, update.increment_retry);
224 assert_eq!(back.cost_usd, update.cost_usd);
225 assert_eq!(back.duration_ms, update.duration_ms);
226 }
227
228 #[test]
229 fn runfilter_default_is_no_filters() {
230 let filter = RunFilter::default();
231 assert!(filter.workflow_name.is_none());
232 assert!(filter.status.is_none());
233 assert!(filter.created_after.is_none());
234 assert!(filter.created_before.is_none());
235 }
236
237 #[test]
238 fn runfilter_with_multiple_criteria() {
239 let filter = RunFilter {
240 workflow_name: Some("deploy".to_string()),
241 status: Some(RunStatus::Running),
242 ..RunFilter::default()
243 };
244
245 assert_eq!(filter.workflow_name, Some("deploy".to_string()));
246 assert_eq!(filter.status, Some(RunStatus::Running));
247 assert!(filter.created_after.is_none());
248 assert!(filter.created_before.is_none());
249 }
250}