use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use super::{FsmState, RunStatus, TriggerKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Run {
pub id: Uuid,
pub workflow_name: String,
pub status: FsmState<RunStatus>,
pub trigger: TriggerKind,
pub payload: Value,
pub error: Option<String>,
pub retry_count: u32,
pub max_retries: u32,
pub cost_usd: Decimal,
pub duration_ms: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewRun {
pub workflow_name: String,
pub trigger: TriggerKind,
pub payload: Value,
pub max_retries: u32,
}
#[derive(Debug, Clone, Default)]
pub struct RunFilter {
pub workflow_name: Option<String>,
pub status: Option<RunStatus>,
pub created_after: Option<DateTime<Utc>>,
pub created_before: Option<DateTime<Utc>>,
pub has_steps: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunUpdate {
pub status: Option<RunStatus>,
pub error: Option<String>,
pub increment_retry: bool,
pub cost_usd: Option<Decimal>,
pub duration_ms: Option<u64>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn newrun_serde_roundtrip() {
let new_run = NewRun {
workflow_name: "deploy".to_string(),
trigger: TriggerKind::Manual,
payload: json!({"key": "value"}),
max_retries: 3,
};
let json = serde_json::to_string(&new_run).expect("serialize");
let back: NewRun = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.workflow_name, new_run.workflow_name);
assert_eq!(back.trigger, new_run.trigger);
assert_eq!(back.payload, new_run.payload);
assert_eq!(back.max_retries, new_run.max_retries);
}
#[test]
fn run_serde_preserves_all_fields() {
use crate::entities::FsmState;
use chrono::Utc;
use uuid::Uuid;
let now = Utc::now();
let run = Run {
id: Uuid::now_v7(),
workflow_name: "test-wf".to_string(),
status: FsmState::new(RunStatus::Running, Uuid::now_v7()),
trigger: TriggerKind::Webhook {
path: "/hooks/test".to_string(),
},
payload: json!({"data": 123}),
error: Some("test error".to_string()),
retry_count: 2,
max_retries: 5,
cost_usd: Decimal::new(1234, 2),
duration_ms: 5000,
created_at: now,
updated_at: now,
started_at: Some(now),
completed_at: Some(now),
};
let json = serde_json::to_string(&run).expect("serialize");
let back: Run = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.id, run.id);
assert_eq!(back.workflow_name, run.workflow_name);
assert_eq!(back.status.state, run.status.state);
assert_eq!(back.trigger, run.trigger);
assert_eq!(back.payload, run.payload);
assert_eq!(back.error, run.error);
assert_eq!(back.retry_count, run.retry_count);
assert_eq!(back.max_retries, run.max_retries);
assert_eq!(back.cost_usd, run.cost_usd);
assert_eq!(back.duration_ms, run.duration_ms);
assert_eq!(back.started_at, run.started_at);
assert_eq!(back.completed_at, run.completed_at);
}
#[test]
fn runupdate_serde_roundtrip() {
let update = RunUpdate {
status: Some(RunStatus::Completed),
error: Some("test error".to_string()),
increment_retry: true,
cost_usd: Some(Decimal::new(5000, 2)),
duration_ms: Some(3000),
started_at: None,
completed_at: None,
};
let json = serde_json::to_string(&update).expect("serialize");
let back: RunUpdate = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.status, update.status);
assert_eq!(back.error, update.error);
assert_eq!(back.increment_retry, update.increment_retry);
assert_eq!(back.cost_usd, update.cost_usd);
assert_eq!(back.duration_ms, update.duration_ms);
}
#[test]
fn runfilter_default_is_no_filters() {
let filter = RunFilter::default();
assert!(filter.workflow_name.is_none());
assert!(filter.status.is_none());
assert!(filter.created_after.is_none());
assert!(filter.created_before.is_none());
}
#[test]
fn runfilter_with_multiple_criteria() {
let filter = RunFilter {
workflow_name: Some("deploy".to_string()),
status: Some(RunStatus::Running),
..RunFilter::default()
};
assert_eq!(filter.workflow_name, Some("deploy".to_string()));
assert_eq!(filter.status, Some(RunStatus::Running));
assert!(filter.created_after.is_none());
assert!(filter.created_before.is_none());
}
}