Skip to main content

af_workflow/
host.rs

1//! Validated multi-branch workflow host.
2
3use std::sync::Arc;
4
5use crate::executor::CompiledBranch;
6use crate::{
7    validate, CompileError, Event, NodeRegistry, RunOutcome, Spec, SpecError, State, Violation,
8    WorkflowContext,
9};
10
11pub const ROOT_BRANCH: &str = "__root__";
12
13#[derive(Debug, thiserror::Error)]
14pub enum HostError {
15    #[error("workflow spec failed to parse: {0}")]
16    Parse(#[from] SpecError),
17    #[error("workflow spec '{spec_id}' has no branches")]
18    NoBranch { spec_id: String },
19    #[error("workflow spec '{spec_id}' violates safety rules: {violations:?}")]
20    Safety {
21        spec_id: String,
22        violations: Vec<Violation>,
23    },
24    #[error("workflow spec '{spec_id}' failed to compile: {source}")]
25    Compile {
26        spec_id: String,
27        #[source]
28        source: CompileError,
29    },
30}
31
32struct BranchEntry {
33    id: String,
34    branch: CompiledBranch,
35}
36
37/// A parsed, validated and compiled workflow. Assembly is the only constructor,
38/// so a long-running host cannot accidentally skip static validation.
39pub struct WorkflowHost {
40    spec_id: String,
41    branches: Vec<BranchEntry>,
42}
43
44impl WorkflowHost {
45    pub fn assemble(spec_json: &str, registry: &NodeRegistry) -> Result<Self, HostError> {
46        Self::from_spec(&Spec::from_json(spec_json)?, registry)
47    }
48
49    pub fn from_spec(spec: &Spec, registry: &NodeRegistry) -> Result<Self, HostError> {
50        if spec.branches.is_empty() {
51            return Err(HostError::NoBranch {
52                spec_id: spec.spec_id.clone(),
53            });
54        }
55        if let Err(violations) = validate(spec, registry) {
56            return Err(HostError::Safety {
57                spec_id: spec.spec_id.clone(),
58                violations,
59            });
60        }
61
62        let branches = spec
63            .branches
64            .iter()
65            .map(|branch| {
66                Ok(BranchEntry {
67                    id: branch.branch_id.clone(),
68                    branch: CompiledBranch::compile(branch, registry).map_err(|source| {
69                        HostError::Compile {
70                            spec_id: spec.spec_id.clone(),
71                            source,
72                        }
73                    })?,
74                })
75            })
76            .collect::<Result<Vec<_>, HostError>>()?;
77        Ok(Self {
78            spec_id: spec.spec_id.clone(),
79            branches,
80        })
81    }
82
83    pub fn spec_id(&self) -> &str {
84        &self.spec_id
85    }
86
87    pub fn branch_id(&self) -> &str {
88        &self.branches[0].id
89    }
90
91    pub fn branch_ids(&self) -> impl Iterator<Item = &str> {
92        self.branches.iter().map(|branch| branch.id.as_str())
93    }
94
95    pub fn branch_count(&self) -> usize {
96        self.branches.len()
97    }
98
99    pub fn context(&self, state: Arc<dyn State>) -> WorkflowContext {
100        self.context_for(self.branch_id(), state)
101    }
102
103    pub fn context_for(&self, branch_id: &str, state: Arc<dyn State>) -> WorkflowContext {
104        WorkflowContext::new(branch_id, state)
105    }
106
107    pub async fn run_event(&self, ctx: &WorkflowContext, event: Event) -> Option<RunOutcome> {
108        self.run_event_on(&ctx.branch_id, ctx, event).await
109    }
110
111    pub async fn run_event_on(
112        &self,
113        branch_id: &str,
114        ctx: &WorkflowContext,
115        event: Event,
116    ) -> Option<RunOutcome> {
117        let branch = self.branches.iter().find(|branch| branch.id == branch_id)?;
118        Some(branch.branch.run_event(ctx, event).await)
119    }
120}