use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub type WorkflowId = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExecutionId(pub Uuid);
impl ExecutionId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for ExecutionId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ExecutionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "exec_{}", self.0.simple())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowStatus {
Pending,
Running,
Paused,
Completed,
Failed,
Cancelled,
LimitExceeded,
}
impl WorkflowStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Cancelled | Self::LimitExceeded
)
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Pending | Self::Running | Self::Paused)
}
pub fn validate_transition(&self, next: &WorkflowStatus) -> crate::error::Result<()> {
let valid = matches!(
(self, next),
(Self::Pending, Self::Running)
| (Self::Running, Self::Paused)
| (Self::Running, Self::Completed)
| (Self::Running, Self::Failed)
| (Self::Running, Self::Cancelled)
| (Self::Running, Self::LimitExceeded)
| (Self::Paused, Self::Running)
| (Self::Paused, Self::Cancelled)
);
if valid {
Ok(())
} else {
Err(crate::Error::InvalidTransition {
current: self.clone(),
requested: next.clone(),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionType {
Stateless,
Resumable,
PersistentGoverned,
Ephemeral,
ApprovalGated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowMetadata {
pub id: WorkflowId,
pub version: String,
pub name: Option<String>,
pub description: Option<String>,
pub state_schema: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowExecution {
pub execution_id: ExecutionId,
pub workflow_id: WorkflowId,
pub workflow_version: String,
pub status: WorkflowStatus,
pub initial_input: serde_json::Value,
pub current_state: serde_json::Value,
pub started_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_type: Option<SessionType>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_transitions() {
let s = WorkflowStatus::Pending;
assert!(s.validate_transition(&WorkflowStatus::Running).is_ok());
}
#[test]
fn invalid_transitions() {
let s = WorkflowStatus::Completed;
assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
}
#[test]
fn terminal_states() {
assert!(WorkflowStatus::Completed.is_terminal());
assert!(WorkflowStatus::Failed.is_terminal());
assert!(WorkflowStatus::Cancelled.is_terminal());
assert!(WorkflowStatus::LimitExceeded.is_terminal());
assert!(!WorkflowStatus::Running.is_terminal());
assert!(!WorkflowStatus::Paused.is_terminal());
}
#[test]
fn limit_exceeded_transition() {
let s = WorkflowStatus::Running;
assert!(s
.validate_transition(&WorkflowStatus::LimitExceeded)
.is_ok());
let s = WorkflowStatus::LimitExceeded;
assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
}
#[test]
fn execution_id_display() {
let id = ExecutionId::new();
assert!(id.to_string().starts_with("exec_"));
}
}