use std::sync::Arc;
use crate::executor::CompiledBranch;
use crate::{
validate, CompileError, Event, NodeRegistry, RunOutcome, Spec, SpecError, State, Violation,
WorkflowContext,
};
pub const ROOT_BRANCH: &str = "__root__";
#[derive(Debug, thiserror::Error)]
pub enum HostError {
#[error("workflow spec failed to parse: {0}")]
Parse(#[from] SpecError),
#[error("workflow spec '{spec_id}' has no branches")]
NoBranch {
spec_id: String,
},
#[error("workflow spec '{spec_id}' violates safety rules: {violations:?}")]
Safety {
spec_id: String,
violations: Vec<Violation>,
},
#[error("workflow spec '{spec_id}' failed to compile: {source}")]
Compile {
spec_id: String,
#[source]
source: CompileError,
},
#[error("workflow spec '{spec_id}' step '{node_id}' ({node_type}) performs an external effect; durable spec execution runs pure/read steps only, register a WorkflowActionProvider for it")]
UnsupportedDurableStep {
spec_id: String,
node_id: String,
node_type: String,
},
#[error("workflow spec '{spec_id}' has an invalid schedule: {reason}")]
Schedule {
spec_id: String,
reason: String,
},
}
struct BranchEntry {
id: String,
branch: CompiledBranch,
}
pub struct WorkflowHost {
spec_id: String,
branches: Vec<BranchEntry>,
}
impl WorkflowHost {
pub fn assemble(spec_json: &str, registry: &NodeRegistry) -> Result<Self, HostError> {
Self::from_spec(&Spec::from_json(spec_json)?, registry)
}
pub fn from_spec(spec: &Spec, registry: &NodeRegistry) -> Result<Self, HostError> {
if spec.branches.is_empty() {
return Err(HostError::NoBranch {
spec_id: spec.spec_id.clone(),
});
}
if let Err(violations) = validate(spec, registry) {
return Err(HostError::Safety {
spec_id: spec.spec_id.clone(),
violations,
});
}
let branches = spec
.branches
.iter()
.map(|branch| {
Ok(BranchEntry {
id: branch.branch_id.clone(),
branch: CompiledBranch::compile(branch, registry).map_err(|source| {
HostError::Compile {
spec_id: spec.spec_id.clone(),
source,
}
})?,
})
})
.collect::<Result<Vec<_>, HostError>>()?;
Ok(Self {
spec_id: spec.spec_id.clone(),
branches,
})
}
pub fn spec_id(&self) -> &str {
&self.spec_id
}
pub fn branch_id(&self) -> &str {
&self.branches[0].id
}
pub fn branch_ids(&self) -> impl Iterator<Item = &str> {
self.branches.iter().map(|branch| branch.id.as_str())
}
pub fn branch_count(&self) -> usize {
self.branches.len()
}
pub fn context(&self, state: Arc<dyn State>) -> WorkflowContext {
self.context_for(self.branch_id(), state)
}
pub fn context_for(&self, branch_id: &str, state: Arc<dyn State>) -> WorkflowContext {
WorkflowContext::new(branch_id, state)
}
pub async fn run_event(&self, ctx: &WorkflowContext, event: Event) -> Option<RunOutcome> {
self.run_event_on(&ctx.branch_id, ctx, event).await
}
pub async fn run_event_on(
&self,
branch_id: &str,
ctx: &WorkflowContext,
event: Event,
) -> Option<RunOutcome> {
let branch = self.branches.iter().find(|branch| branch.id == branch_id)?;
Some(branch.branch.run_event(ctx, event).await)
}
}