use crate::FlowRef;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct WorkflowRef {
pub namespace: String,
pub workflow_id: String,
pub version: u64,
}
impl WorkflowRef {
pub fn new(namespace: impl Into<String>, workflow_id: impl Into<String>, version: u64) -> Self {
Self {
namespace: namespace.into(),
workflow_id: workflow_id.into(),
version,
}
}
pub fn canonical(&self) -> String {
format!(
"flow://{}/{}@{}",
self.namespace, self.workflow_id, self.version
)
}
pub fn from_canonical(s: &str) -> Option<Self> {
if !s.starts_with("flow://") {
return None;
}
let s = &s[7..];
let (namespace, rest) = s.split_once('/')?;
let (workflow_id, version_str) = rest.split_once('@')?;
let version = version_str.parse().ok()?;
Some(Self {
namespace: namespace.to_string(),
workflow_id: workflow_id.to_string(),
version,
})
}
}
impl Display for WorkflowRef {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct WorkflowStepRef {
pub workflow_ref: WorkflowRef,
pub step_id: String,
}
impl WorkflowStepRef {
pub fn new(workflow_ref: WorkflowRef, step_id: impl Into<String>) -> Self {
Self {
workflow_ref,
step_id: step_id.into(),
}
}
pub fn canonical(&self) -> String {
format!("{}/{}", self.workflow_ref.canonical(), self.step_id)
}
pub fn from_canonical(s: &str) -> Option<Self> {
if !s.starts_with("flow://") {
return None;
}
let s = &s[7..];
let last_slash_idx = s.rfind('/')?;
let workflow_str = &s[..last_slash_idx];
let step_id = &s[last_slash_idx + 1..];
let (namespace, rest) = workflow_str.split_once('/')?;
let (workflow_id, version_str) = rest.split_once('@')?;
let version = version_str.parse().ok()?;
Some(Self {
workflow_ref: WorkflowRef {
namespace: namespace.to_string(),
workflow_id: workflow_id.to_string(),
version,
},
step_id: step_id.to_string(),
})
}
}
impl Display for WorkflowStepRef {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum WorkflowStepState {
Pending,
Ready,
Claimed,
Running,
Completed,
Failed,
Retrying,
Blocked,
}
impl Display for WorkflowStepState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => write!(f, "Pending"),
Self::Ready => write!(f, "Ready"),
Self::Claimed => write!(f, "Claimed"),
Self::Running => write!(f, "Running"),
Self::Completed => write!(f, "Completed"),
Self::Failed => write!(f, "Failed"),
Self::Retrying => write!(f, "Retrying"),
Self::Blocked => write!(f, "Blocked"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum BlockedReason {
AwaitingApproval(String),
AwaitingInput(String),
AwaitingCapability(String),
AwaitingResource(String),
Custom(String),
}
impl Display for BlockedReason {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::AwaitingApproval(msg) => write!(f, "AwaitingApproval: {}", msg),
Self::AwaitingInput(msg) => write!(f, "AwaitingInput: {}", msg),
Self::AwaitingCapability(cap) => write!(f, "AwaitingCapability: {}", cap),
Self::AwaitingResource(res) => write!(f, "AwaitingResource: {}", res),
Self::Custom(msg) => write!(f, "Custom: {}", msg),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum WorkflowState {
Created,
Running,
Completed,
Failed,
}
impl Display for WorkflowState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Created => write!(f, "Created"),
Self::Running => write!(f, "Running"),
Self::Completed => write!(f, "Completed"),
Self::Failed => write!(f, "Failed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum WorkflowOperation {
Created { workflow_ref: WorkflowRef },
Started { workflow_ref: WorkflowRef },
StepReady { step_ref: WorkflowStepRef },
StepClaimed {
step_ref: WorkflowStepRef,
claimed_by: String,
},
StepExecuting {
step_ref: WorkflowStepRef,
claimed_by: String,
},
StepCompleted {
step_ref: WorkflowStepRef,
result_ref: Option<FlowRef>,
},
StepFailed {
step_ref: WorkflowStepRef,
error: String,
attempt: u32,
},
StepBlocked {
step_ref: WorkflowStepRef,
reason: BlockedReason,
},
StepRetrying {
step_ref: WorkflowStepRef,
attempt: u32,
},
Completed { workflow_ref: WorkflowRef },
Failed {
workflow_ref: WorkflowRef,
error: String,
},
}
impl WorkflowOperation {
pub fn workflow_ref(&self) -> Option<&WorkflowRef> {
match self {
Self::Created { workflow_ref } => Some(workflow_ref),
Self::Started { workflow_ref } => Some(workflow_ref),
Self::Completed { workflow_ref } => Some(workflow_ref),
Self::Failed { workflow_ref, .. } => Some(workflow_ref),
_ => None,
}
}
pub fn step_ref(&self) -> Option<&WorkflowStepRef> {
match self {
Self::StepReady { step_ref } => Some(step_ref),
Self::StepClaimed { step_ref, .. } => Some(step_ref),
Self::StepExecuting { step_ref, .. } => Some(step_ref),
Self::StepCompleted { step_ref, .. } => Some(step_ref),
Self::StepFailed { step_ref, .. } => Some(step_ref),
Self::StepBlocked { step_ref, .. } => Some(step_ref),
Self::StepRetrying { step_ref, .. } => Some(step_ref),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowNode {
pub step_id: String,
pub capability: String,
pub inputs: Vec<String>, pub outputs: Vec<String>,
pub state_requirements: HashMap<String, String>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl WorkflowNode {
pub fn new(step_id: impl Into<String>, capability: impl Into<String>) -> Self {
Self {
step_id: step_id.into(),
capability: capability.into(),
inputs: Vec::new(),
outputs: Vec::new(),
state_requirements: HashMap::new(),
metadata: HashMap::new(),
}
}
pub fn with_input(mut self, input: impl Into<String>) -> Self {
self.inputs.push(input.into());
self
}
pub fn with_output(mut self, output: impl Into<String>) -> Self {
self.outputs.push(output.into());
self
}
pub fn with_state_requirement(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.state_requirements.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowGraph {
pub workflow_ref: WorkflowRef,
pub nodes: HashMap<String, WorkflowNode>,
pub edges: HashMap<String, Vec<String>>,
pub inputs: Vec<FlowRef>,
pub outputs: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl WorkflowGraph {
pub fn new(workflow_ref: WorkflowRef) -> Self {
Self {
workflow_ref,
nodes: HashMap::new(),
edges: HashMap::new(),
inputs: Vec::new(),
outputs: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn add_node(mut self, node: WorkflowNode) -> Self {
self.nodes.insert(node.step_id.clone(), node);
self
}
pub fn add_edge(mut self, from_step: impl Into<String>, to_step: impl Into<String>) -> Self {
let from = from_step.into();
let to = to_step.into();
self.edges.entry(from).or_insert_with(Vec::new).push(to);
self
}
pub fn with_input(mut self, input: FlowRef) -> Self {
self.inputs.push(input);
self
}
pub fn validate(&self) -> Result<(), String> {
for (from, tos) in &self.edges {
if !self.nodes.contains_key(from) {
return Err(format!("Edge source '{}' does not exist", from));
}
for to in tos {
if !self.nodes.contains_key(to) {
return Err(format!("Edge target '{}' does not exist", to));
}
}
}
for node in self.nodes.values() {
for input in &node.inputs {
if !self.nodes.contains_key(input) && !input.starts_with("input:") {
return Err(format!(
"Node '{}' has input dependency '{}' that doesn't exist",
node.step_id, input
));
}
}
}
self.detect_cycles()?;
Ok(())
}
fn detect_cycles(&self) -> Result<(), String> {
let mut visited: HashSet<String> = HashSet::new();
let mut rec_stack: HashSet<String> = HashSet::new();
for node_id in self.nodes.keys() {
if !visited.contains(node_id) {
self.dfs(node_id, &mut visited, &mut rec_stack)?;
}
}
Ok(())
}
fn dfs(
&self,
node_id: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> Result<(), String> {
visited.insert(node_id.to_string());
rec_stack.insert(node_id.to_string());
if let Some(neighbors) = self.edges.get(node_id) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
self.dfs(neighbor, visited, rec_stack)?;
} else if rec_stack.contains(neighbor) {
return Err(format!("Cycle detected: {} -> {}", node_id, neighbor));
}
}
}
rec_stack.remove(node_id);
Ok(())
}
pub fn entry_nodes(&self) -> Vec<String> {
self.nodes
.keys()
.filter(|node_id| {
if let Some(node) = self.nodes.get(*node_id) {
node.inputs.is_empty()
|| node.inputs.iter().all(|inp| inp.starts_with("input:"))
} else {
false
}
})
.cloned()
.collect()
}
pub fn dependents(&self, step_id: &str) -> Vec<String> {
self.edges
.get(step_id)
.map(|deps| deps.clone())
.unwrap_or_default()
}
pub fn dependencies(&self, step_id: &str) -> Vec<String> {
if let Some(node) = self.nodes.get(step_id) {
node.inputs
.iter()
.filter(|inp| !inp.starts_with("input:"))
.cloned()
.collect()
} else {
Vec::new()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowInstance {
pub workflow_ref: WorkflowRef,
pub instance_id: String,
pub state: WorkflowState,
pub step_states: HashMap<String, WorkflowStepState>,
pub step_claims: HashMap<String, String>,
pub step_results: HashMap<String, FlowRef>,
pub step_failures: HashMap<String, Vec<String>>,
pub step_blocked_reasons: HashMap<String, BlockedReason>,
pub created_ms: u128,
pub started_ms: Option<u128>,
pub completed_ms: Option<u128>,
}
impl WorkflowInstance {
pub fn new(workflow_ref: WorkflowRef, instance_id: impl Into<String>) -> Self {
Self {
workflow_ref,
instance_id: instance_id.into(),
state: WorkflowState::Created,
step_states: HashMap::new(),
step_claims: HashMap::new(),
step_results: HashMap::new(),
step_failures: HashMap::new(),
step_blocked_reasons: HashMap::new(),
created_ms: crate::now_ms(),
started_ms: None,
completed_ms: None,
}
}
pub fn get_step_state(&self, step_id: &str) -> Option<WorkflowStepState> {
self.step_states.get(step_id).copied()
}
pub fn set_step_state(&mut self, step_id: impl Into<String>, state: WorkflowStepState) {
self.step_states.insert(step_id.into(), state);
}
pub fn claim_step(
&mut self,
step_id: impl Into<String>,
claimed_by: impl Into<String>,
) -> Result<(), String> {
let step_id = step_id.into();
let claimed_by = claimed_by.into();
match self.get_step_state(&step_id) {
Some(WorkflowStepState::Ready) => {
self.set_step_state(step_id.clone(), WorkflowStepState::Claimed);
self.step_claims.insert(step_id, claimed_by);
Ok(())
}
Some(state) => Err(format!("Cannot claim step in state {}", state)),
None => Err(format!("Step not found: {}", step_id)),
}
}
pub fn complete_step(
&mut self,
step_id: impl Into<String>,
result_ref: FlowRef,
) -> Result<(), String> {
let step_id = step_id.into();
self.set_step_state(step_id.clone(), WorkflowStepState::Completed);
self.step_results.insert(step_id, result_ref);
Ok(())
}
pub fn fail_step(&mut self, step_id: impl Into<String>, error: String) -> Result<(), String> {
let step_id = step_id.into();
self.set_step_state(step_id.clone(), WorkflowStepState::Failed);
self.step_failures
.entry(step_id)
.or_insert_with(Vec::new)
.push(error);
Ok(())
}
pub fn is_terminal(&self) -> bool {
matches!(self.state, WorkflowState::Completed | WorkflowState::Failed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_ref_canonical() {
let r = WorkflowRef::new("workflow", "order-processing", 1);
assert_eq!(r.canonical(), "flow://workflow/order-processing@1");
}
#[test]
fn test_workflow_ref_parse() {
let s = "flow://workflow/order-processing@1";
let r = WorkflowRef::from_canonical(s).unwrap();
assert_eq!(r.namespace, "workflow");
assert_eq!(r.workflow_id, "order-processing");
assert_eq!(r.version, 1);
}
#[test]
fn test_workflow_step_ref_canonical() {
let wf = WorkflowRef::new("workflow", "test", 1);
let step = WorkflowStepRef::new(wf, "step-1");
assert_eq!(step.canonical(), "flow://workflow/test@1/step-1");
}
#[test]
fn test_workflow_step_ref_parse() {
let s = "flow://workflow/test@1/step-1";
let step = WorkflowStepRef::from_canonical(s).unwrap();
assert_eq!(step.step_id, "step-1");
assert_eq!(step.workflow_ref.workflow_id, "test");
}
#[test]
fn test_workflow_graph_validation() {
let wf = WorkflowRef::new("workflow", "test", 1);
let graph = WorkflowGraph::new(wf)
.add_node(WorkflowNode::new("step-1", "capability-a"))
.add_node(WorkflowNode::new("step-2", "capability-b").with_input("step-1"))
.add_edge("step-1", "step-2");
assert!(graph.validate().is_ok());
}
#[test]
fn test_workflow_graph_cycle_detection() {
let wf = WorkflowRef::new("workflow", "test", 1);
let graph = WorkflowGraph::new(wf)
.add_node(WorkflowNode::new("step-1", "capability-a"))
.add_node(WorkflowNode::new("step-2", "capability-b"))
.add_edge("step-1", "step-2")
.add_edge("step-2", "step-1");
assert!(graph.validate().is_err());
}
#[test]
fn test_workflow_graph_entry_nodes() {
let wf = WorkflowRef::new("workflow", "test", 1);
let graph = WorkflowGraph::new(wf)
.add_node(WorkflowNode::new("step-1", "capability-a"))
.add_node(WorkflowNode::new("step-2", "capability-b").with_input("step-1"))
.add_edge("step-1", "step-2");
let entries = graph.entry_nodes();
assert_eq!(entries.len(), 1);
assert!(entries.contains(&"step-1".to_string()));
}
#[test]
fn test_workflow_instance_lifecycle() {
let wf = WorkflowRef::new("workflow", "test", 1);
let mut instance = WorkflowInstance::new(wf, "instance-1");
instance.set_step_state("step-1", WorkflowStepState::Pending);
assert_eq!(
instance.get_step_state("step-1"),
Some(WorkflowStepState::Pending)
);
instance.set_step_state("step-1", WorkflowStepState::Ready);
let claim_result = instance.claim_step("step-1", "peer-a");
assert!(claim_result.is_ok());
assert_eq!(
instance.get_step_state("step-1"),
Some(WorkflowStepState::Claimed)
);
}
#[test]
fn test_workflow_blocked_reason() {
let reason = BlockedReason::AwaitingApproval("Needs manager approval".to_string());
assert_eq!(
reason.to_string(),
"AwaitingApproval: Needs manager approval"
);
}
}