use crate::conditional::builder::ConditionalScopeBuilder;
use crate::core::context::{AnyContextDataExtractor, Handler};
use crate::core::context_data::ContextData;
use crate::core::step::{SkipCondition, StepDef};
use crate::error::{OrkaError, OrkaResult};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
pub struct Pipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<crate::error::OrkaError> + Send + Sync + 'static,
{
pub(crate) steps: Vec<StepDef<TData>>,
pub(crate) before: HashMap<String, Vec<Handler<TData, Err>>>,
pub(crate) on: HashMap<String, Vec<Handler<TData, Err>>>,
pub(crate) after: HashMap<String, Vec<Handler<TData, Err>>>,
pub(crate) extractors: HashMap<String, Arc<dyn AnyContextDataExtractor<TData>>>,
pub(crate) pending_conditional: HashSet<String>,
pub(crate) sub_handler_steps: HashSet<String>,
}
impl<TData, Err> Pipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<crate::error::OrkaError> + Send + Sync + 'static,
{
pub fn new<I, S>(step_names: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut steps: Vec<StepDef<TData>> = Vec::new();
for name in step_names {
let name = name.as_ref().to_string();
if steps.iter().any(|s: &StepDef<TData>| s.name == name) {
panic!("Orka setup error: duplicate step '{}' in pipeline definition.", name);
}
steps.push(StepDef {
name,
optional: false,
skip_if: None,
});
}
Self {
steps,
before: HashMap::new(),
on: HashMap::new(),
after: HashMap::new(),
extractors: HashMap::new(),
pending_conditional: HashSet::new(),
sub_handler_steps: HashSet::new(),
}
}
pub(crate) fn ensure_step_exists(&self, step_name: &str) {
if !self.steps.iter().any(|s| s.name == step_name) {
panic!(
"Orka setup error: Step '{}' not found in pipeline definition.",
step_name
);
}
}
fn ensure_step_not_exists(&self, step_name: &str) {
if self.steps.iter().any(|s| s.name == step_name) {
panic!(
"Orka setup error: Step '{}' already exists in pipeline definition.",
step_name
);
}
}
pub fn insert_before_step<S: Into<String>>(&mut self, existing_step_name: &str, new_step_name: S) -> &mut Self {
self.ensure_step_exists(existing_step_name); let idx = self.steps.iter().position(|s| s.name == existing_step_name).unwrap(); let name_str: String = new_step_name.into();
self.ensure_step_not_exists(&name_str); self.steps.insert(
idx,
StepDef {
name: name_str,
optional: false,
skip_if: None,
},
);
self
}
pub fn insert_after_step<S: Into<String>>(&mut self, existing_step_name: &str, new_step_name: S) -> &mut Self {
self.ensure_step_exists(existing_step_name);
let idx = self.steps.iter().position(|s| s.name == existing_step_name).unwrap();
let name_str: String = new_step_name.into();
self.ensure_step_not_exists(&name_str);
self.steps.insert(
idx + 1,
StepDef {
name: name_str,
optional: false,
skip_if: None,
},
);
self
}
pub fn remove_step(&mut self, step_name: &str) -> &mut Self {
if let Some(idx) = self.steps.iter().position(|s| s.name == step_name) {
self.steps.remove(idx);
self.before.remove(step_name);
self.on.remove(step_name);
self.after.remove(step_name);
self.extractors.remove(step_name);
self.pending_conditional.remove(step_name);
self.sub_handler_steps.remove(step_name);
}
self
}
pub fn optional(&mut self, step_name: &str) -> &mut Self {
self.ensure_step_exists(step_name);
self.steps.iter_mut().find(|s| s.name == step_name).unwrap().optional = true;
self
}
pub fn required(&mut self, step_name: &str) -> &mut Self {
self.ensure_step_exists(step_name);
self.steps.iter_mut().find(|s| s.name == step_name).unwrap().optional = false;
self
}
pub fn skip_if(
&mut self,
step_name: &str,
cond: impl Fn(ContextData<TData>) -> bool + Send + Sync + 'static,
) -> &mut Self {
self.ensure_step_exists(step_name);
let skip: SkipCondition<TData> = Arc::new(cond);
self.steps.iter_mut().find(|s| s.name == step_name).unwrap().skip_if = Some(skip);
self
}
pub fn clear_skip_condition(&mut self, step_name: &str) -> &mut Self {
self.ensure_step_exists(step_name);
self.steps.iter_mut().find(|s| s.name == step_name).unwrap().skip_if = None;
self
}
pub fn validate(&self) -> OrkaResult<()> {
let mut problems: Vec<(String, String)> = Vec::new();
for step in &self.steps {
let name = step.name.as_str();
let has_handlers = [&self.before, &self.on, &self.after]
.iter()
.any(|m| m.get(name).is_some_and(|v| !v.is_empty()));
if !step.optional && !has_handlers {
problems.push((
step.name.clone(),
format!(
"required step '{}' has no before/on/after handlers; register one or mark it optional",
name
),
));
}
}
for step_name in self.extractors.keys() {
if !self.sub_handler_steps.contains(step_name) {
problems.push((
step_name.clone(),
format!(
"step '{}' has an extractor but no on::<SData> handler consuming it",
step_name
),
));
}
}
for step_name in &self.pending_conditional {
problems.push((
step_name.clone(),
format!(
"step '{}' called conditional_scopes_for_step but never finalize_conditional_step(); its scopes were discarded",
step_name
),
));
}
match problems.len() {
0 => Ok(()),
1 => {
let (step_name, message) = problems.pop().unwrap();
Err(OrkaError::ConfigurationError { step_name, message })
}
n => {
let message = problems
.iter()
.map(|(_, m)| format!(" - {}", m))
.collect::<Vec<_>>()
.join("\n");
Err(OrkaError::ConfigurationError {
step_name: format!("<{} steps>", n),
message: format!("pipeline validation found {} problems:\n{}", n, message),
})
}
}
}
pub fn conditional_scopes_for_step(&mut self, step_name: &str) -> ConditionalScopeBuilder<'_, TData, Err> {
if !self.steps.iter().any(|s| s.name == step_name) {
self.steps.push(StepDef {
name: step_name.to_string(),
optional: false, skip_if: None,
});
}
ConditionalScopeBuilder::new(self, step_name.to_string())
}
}