mod edges;
mod os_scope;
mod placeholder;
mod run_time;
mod when;
use crate::executor::field_scope::{FieldValues, declare};
use crate::executor::resolve::resolve_fields;
use crate::missing_fields::missing_fields;
use crate::pipeline::{PipelineDefinition, PipelineNode, is_container_node, is_io_node};
use crate::processor::NodeProcessor;
use crate::registry::NodeRegistry;
use crate::renamed::with_node_type_hint;
use crate::undeclared_params::undeclared_params;
use edges::{Level, edge_findings};
pub use run_time::RunTimeParam;
use run_time::{LeftOut, Subject, left_out};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckFinding {
pub node_id: String,
pub node_type: String,
pub param: Option<String>,
pub message: String,
}
impl std::fmt::Display for CheckFinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "node \"{}\" ({})", self.node_id, self.node_type)?;
if let Some(param) = &self.param {
write!(f, ", param \"{param}\"")?;
}
write!(f, ": {}", self.message)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CheckReport {
pub findings: Vec<CheckFinding>,
pub run_time: Vec<RunTimeParam>,
}
pub fn check_pipeline(definition: &PipelineDefinition, registry: &NodeRegistry) -> CheckReport {
let mut report = CheckReport::default();
let root = Level {
nodes: &definition.nodes,
edges: &definition.edges,
owner: None,
};
let recipe = Recipe {
root: &definition.nodes,
registry,
};
let scope = Scope {
fields: FieldValues::new(),
platforms: os_scope::PLATFORMS.to_vec(),
};
check_level(&root, &scope, &recipe, &mut report);
report
.findings
.extend(undeclared_field_findings(&definition.nodes));
report
}
fn undeclared_field_findings(nodes: &[PipelineNode]) -> Vec<CheckFinding> {
missing_fields(nodes)
.into_iter()
.filter(|m| m.declared_by.is_none())
.map(|m| CheckFinding {
message: m.message(),
node_id: m.node_id,
node_type: m.node_type,
param: None,
})
.collect()
}
struct Scope {
fields: FieldValues,
platforms: Vec<&'static str>,
}
impl Scope {
fn enter(&self, node: &PipelineNode) -> Self {
Self {
fields: declare(&self.fields, &node.fields),
platforms: os_scope::narrow(&self.platforms, node),
}
}
}
struct Recipe<'a> {
root: &'a [PipelineNode],
registry: &'a NodeRegistry,
}
fn check_level(level: &Level<'_>, scope: &Scope, recipe: &Recipe<'_>, report: &mut CheckReport) {
report.findings.extend(edge_findings(level, recipe.root));
for node in level.nodes {
check_node(node, &scope.enter(node), recipe, report);
}
}
#[expect(clippy::too_many_lines)]
fn check_node(node: &PipelineNode, scope: &Scope, recipe: &Recipe<'_>, report: &mut CheckReport) {
if is_io_node(&node.node_type) {
report.findings.extend(io_always_finding(node));
report.findings.extend(when::io_when_finding(node));
report.findings.extend(os_scope::io_requires_finding(node));
return;
}
report.findings.extend(when::when_findings(node));
report.findings.extend(os_scope::guard_finding(node));
if is_container_node(&node.node_type) {
report.findings.extend(placeholder_findings(node));
let children = Level {
nodes: node.children.as_deref().unwrap_or_default(),
edges: &node.edges,
owner: Some(node),
};
check_level(&children, scope, recipe, report);
} else {
let unreachable = os_scope::unreachable_finding(node, &scope.platforms);
report.findings.extend(unreachable);
check_processor(node, scope, recipe, report);
report.findings.extend(placeholder_findings(node));
}
}
#[expect(clippy::too_many_lines)]
fn check_processor(
node: &PipelineNode,
scope: &Scope,
recipe: &Recipe<'_>,
report: &mut CheckReport,
) {
let Some(processor) = recipe.registry.resolve(&node.node_type, &node.params) else {
report.findings.push(unknown_type_finding(node));
return;
};
report.findings.extend(undeclared_findings(node, processor));
let raw = processor.validate_raw(&node.params).into_iter();
let on_platforms = processor.platform_problems(&node.params, &scope.platforms);
report.findings.extend(
raw.chain(on_platforms)
.map(|problem| finding(node, None, problem)),
);
let params = resolve_fields(&node.params, &scope.fields);
let written = processor.validate(¶ms);
if written.is_empty() {
return;
}
let subject = Subject {
node,
processor,
params: ¶ms,
};
add_left_out(node, left_out(&subject, written, recipe.root), report);
}
fn add_left_out(node: &PipelineNode, left: LeftOut, report: &mut CheckReport) {
let problems = left.problems.into_iter().map(|m| finding(node, None, m));
report.findings.extend(problems);
for (param, reason) in left.blocked {
let message = format!("required, and no run can give it: {reason}");
report.findings.push(finding(node, Some(¶m), message));
}
report
.run_time
.extend(left.givable.into_iter().map(|param| RunTimeParam {
node_id: node.id.clone(),
node_type: node.node_type.clone(),
param,
}));
}
fn io_always_finding(node: &PipelineNode) -> Option<CheckFinding> {
let message = "\"always\" has no effect on an input or output node; \
mark the processing steps that should run after a failure";
node.always
.then(|| finding(node, None, message.to_string()))
}
fn placeholder_findings(node: &PipelineNode) -> Vec<CheckFinding> {
node.params
.iter()
.flat_map(|(key, value)| {
placeholder::value_problems(value)
.into_iter()
.map(move |message| finding(node, Some(key), message))
})
.collect()
}
fn unknown_type_finding(node: &PipelineNode) -> CheckFinding {
let message = format!("unknown node type \"{}\"", node.node_type);
finding(node, None, with_node_type_hint(message, &node.node_type))
}
fn undeclared_findings(node: &PipelineNode, processor: &dyn NodeProcessor) -> Vec<CheckFinding> {
let Some(declared) = processor.declared_params() else {
return Vec::new();
};
undeclared_params(&node.node_type, &node.params, &declared)
.into_iter()
.map(|u| finding(node, Some(&u.param), u.message))
.collect()
}
fn finding(node: &PipelineNode, param: Option<&str>, message: String) -> CheckFinding {
CheckFinding {
node_id: node.id.clone(),
node_type: node.node_type.clone(),
param: param.map(str::to_string),
message,
}
}
#[cfg(test)]
mod tests;