use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use ironflow_store::models::{RunStatus, StepKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Event {
RunCreated {
run_id: Uuid,
workflow_name: String,
at: DateTime<Utc>,
},
RunStatusChanged {
run_id: Uuid,
workflow_name: String,
from: RunStatus,
to: RunStatus,
error: Option<String>,
cost_usd: Decimal,
duration_ms: u64,
at: DateTime<Utc>,
},
RunFailed {
run_id: Uuid,
workflow_name: String,
error: Option<String>,
cost_usd: Decimal,
duration_ms: u64,
at: DateTime<Utc>,
},
StepCompleted {
run_id: Uuid,
step_id: Uuid,
step_name: String,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
kind: StepKind,
duration_ms: u64,
cost_usd: Decimal,
at: DateTime<Utc>,
},
StepFailed {
run_id: Uuid,
step_id: Uuid,
step_name: String,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
kind: StepKind,
error: String,
at: DateTime<Utc>,
},
ApprovalRequested {
run_id: Uuid,
step_id: Uuid,
message: String,
at: DateTime<Utc>,
},
ApprovalGranted {
run_id: Uuid,
approved_by: String,
at: DateTime<Utc>,
},
ApprovalRejected {
run_id: Uuid,
rejected_by: String,
at: DateTime<Utc>,
},
UserSignedIn {
user_id: Uuid,
username: String,
at: DateTime<Utc>,
},
UserSignedUp {
user_id: Uuid,
username: String,
at: DateTime<Utc>,
},
UserSignedOut {
user_id: Uuid,
at: DateTime<Utc>,
},
}
impl Event {
pub const RUN_CREATED: &'static str = "run_created";
pub const RUN_STATUS_CHANGED: &'static str = "run_status_changed";
pub const RUN_FAILED: &'static str = "run_failed";
pub const STEP_COMPLETED: &'static str = "step_completed";
pub const STEP_FAILED: &'static str = "step_failed";
pub const APPROVAL_REQUESTED: &'static str = "approval_requested";
pub const APPROVAL_GRANTED: &'static str = "approval_granted";
pub const APPROVAL_REJECTED: &'static str = "approval_rejected";
pub const USER_SIGNED_IN: &'static str = "user_signed_in";
pub const USER_SIGNED_UP: &'static str = "user_signed_up";
pub const USER_SIGNED_OUT: &'static str = "user_signed_out";
pub const ALL: &'static [&'static str] = &[
Self::RUN_CREATED,
Self::RUN_STATUS_CHANGED,
Self::RUN_FAILED,
Self::STEP_COMPLETED,
Self::STEP_FAILED,
Self::APPROVAL_REQUESTED,
Self::APPROVAL_GRANTED,
Self::APPROVAL_REJECTED,
Self::USER_SIGNED_IN,
Self::USER_SIGNED_UP,
Self::USER_SIGNED_OUT,
];
pub fn event_type(&self) -> &'static str {
match self {
Event::RunCreated { .. } => Self::RUN_CREATED,
Event::RunStatusChanged { .. } => Self::RUN_STATUS_CHANGED,
Event::RunFailed { .. } => Self::RUN_FAILED,
Event::StepCompleted { .. } => Self::STEP_COMPLETED,
Event::StepFailed { .. } => Self::STEP_FAILED,
Event::ApprovalRequested { .. } => Self::APPROVAL_REQUESTED,
Event::ApprovalGranted { .. } => Self::APPROVAL_GRANTED,
Event::ApprovalRejected { .. } => Self::APPROVAL_REJECTED,
Event::UserSignedIn { .. } => Self::USER_SIGNED_IN,
Event::UserSignedUp { .. } => Self::USER_SIGNED_UP,
Event::UserSignedOut { .. } => Self::USER_SIGNED_OUT,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_status_changed_serde_roundtrip() {
let event = Event::RunStatusChanged {
run_id: Uuid::now_v7(),
workflow_name: "deploy".to_string(),
from: RunStatus::Running,
to: RunStatus::Completed,
error: None,
cost_usd: Decimal::new(42, 2),
duration_ms: 5000,
at: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
let back: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.event_type(), "run_status_changed");
assert!(json.contains("\"type\":\"run_status_changed\""));
}
#[test]
fn run_failed_serde_roundtrip() {
let event = Event::RunFailed {
run_id: Uuid::now_v7(),
workflow_name: "deploy".to_string(),
error: Some("step crashed".to_string()),
cost_usd: Decimal::new(10, 2),
duration_ms: 3000,
at: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
let back: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.event_type(), "run_failed");
assert!(json.contains("\"type\":\"run_failed\""));
assert!(json.contains("step crashed"));
}
#[test]
fn user_signed_in_serde_roundtrip() {
let event = Event::UserSignedIn {
user_id: Uuid::now_v7(),
username: "alice".to_string(),
at: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
let back: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.event_type(), "user_signed_in");
assert!(json.contains("alice"));
}
#[test]
fn step_failed_serde_roundtrip() {
let event = Event::StepFailed {
run_id: Uuid::now_v7(),
step_id: Uuid::now_v7(),
step_name: "build".to_string(),
kind: StepKind::Shell,
error: "exit code 1".to_string(),
at: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
let back: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.event_type(), "step_failed");
}
#[test]
fn approval_requested_serde_roundtrip() {
let event = Event::ApprovalRequested {
run_id: Uuid::now_v7(),
step_id: Uuid::now_v7(),
message: "Deploy to prod?".to_string(),
at: Utc::now(),
};
let json = serde_json::to_string(&event).expect("serialize");
assert!(json.contains("approval_requested"));
}
#[test]
fn event_type_all_variants() {
let id = Uuid::now_v7();
let now = Utc::now();
let cases: Vec<(Event, &str)> = vec![
(
Event::RunCreated {
run_id: id,
workflow_name: "w".to_string(),
at: now,
},
"run_created",
),
(
Event::RunStatusChanged {
run_id: id,
workflow_name: "w".to_string(),
from: RunStatus::Pending,
to: RunStatus::Running,
error: None,
cost_usd: Decimal::ZERO,
duration_ms: 0,
at: now,
},
"run_status_changed",
),
(
Event::RunFailed {
run_id: id,
workflow_name: "w".to_string(),
error: Some("boom".to_string()),
cost_usd: Decimal::ZERO,
duration_ms: 0,
at: now,
},
"run_failed",
),
(
Event::StepCompleted {
run_id: id,
step_id: id,
step_name: "s".to_string(),
kind: StepKind::Shell,
duration_ms: 0,
cost_usd: Decimal::ZERO,
at: now,
},
"step_completed",
),
(
Event::StepFailed {
run_id: id,
step_id: id,
step_name: "s".to_string(),
kind: StepKind::Shell,
error: "err".to_string(),
at: now,
},
"step_failed",
),
(
Event::ApprovalRequested {
run_id: id,
step_id: id,
message: "ok?".to_string(),
at: now,
},
"approval_requested",
),
(
Event::ApprovalGranted {
run_id: id,
approved_by: "alice".to_string(),
at: now,
},
"approval_granted",
),
(
Event::ApprovalRejected {
run_id: id,
rejected_by: "bob".to_string(),
at: now,
},
"approval_rejected",
),
(
Event::UserSignedIn {
user_id: id,
username: "u".to_string(),
at: now,
},
"user_signed_in",
),
(
Event::UserSignedUp {
user_id: id,
username: "u".to_string(),
at: now,
},
"user_signed_up",
),
(
Event::UserSignedOut {
user_id: id,
at: now,
},
"user_signed_out",
),
];
for (event, expected_type) in cases {
assert_eq!(event.event_type(), expected_type);
}
}
}