pub mod answers;
mod config;
pub mod references;
pub mod suggest;
use std::fmt;
use serde::Serialize;
pub use config::{LITERAL_FLAGS, check_node_config, provider_credential};
use crate::pipeline::NodeDefinition;
use crate::pipeline::model::PipelineGraph;
use crate::pipeline::nodes::shared::profile;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Problem {
#[serde(skip_serializing_if = "String::is_empty")]
pub node_id: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flag: Option<String>,
pub message: String,
}
impl Problem {
pub fn node(node_id: &str, kind: &str, flag: Option<&str>, message: String) -> Self {
Self { node_id: node_id.to_string(), kind: kind.to_string(), flag: flag.map(str::to_string), message }
}
pub fn pipeline(message: String) -> Self {
Self { node_id: String::new(), kind: String::new(), flag: None, message }
}
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.node_id.is_empty() {
write!(f, "{}", self.message)
} else {
write!(f, "node `{}`: {}", self.node_id, self.message)
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PipelineCheck {
pub refusals: Vec<Problem>,
pub warnings: Vec<Problem>,
}
impl PipelineCheck {
pub fn is_clean(&self) -> bool {
self.refusals.is_empty() && self.warnings.is_empty()
}
pub fn refusal_message(&self, file: &str) -> String {
let count = self.refusals.len();
let mut out = format!(
"pipeline '{file}' is refused — {count} problem{}:",
if count == 1 { "" } else { "s" }
);
for problem in &self.refusals {
out.push_str(&format!("\n- {problem}"));
}
out
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Catalogue {
Complete,
Official,
}
pub fn check_graph_nodes(
graph: &PipelineGraph,
defs: &[NodeDefinition],
catalogue: Catalogue,
credential_kind: &dyn Fn(&str) -> Option<String>,
) -> Vec<Problem> {
let mut problems = Vec::new();
for node in &graph.nodes {
let Some(def) = defs.iter().find(|d| d.kind == node.kind) else {
if catalogue == Catalogue::Official && node.kind.starts_with("x.") {
continue;
}
let hint = suggest::did_you_mean_phrase(&suggest::suggest_kinds(&node.kind, defs, 3));
problems.push(Problem::node(
&node.id,
&node.kind,
None,
format!("unknown node kind `{}`{hint} — help(\"pipeline/nodes\") lists every kind", node.kind),
));
continue;
};
for mut problem in check_node_config(def, &node.config) {
problem.node_id = node.id.clone();
problems.push(problem);
}
if let Some((provider, id)) = provider_credential(def, &node.config)
&& let Some(kind) = credential_kind(&id)
&& let Err(message) = profile::check_credential(def, &provider, &id, &kind)
{
problems.push(Problem::node(&node.id, &node.kind, Some(profile::CREDENTIAL_FLAG), format!("{} {message}", node.kind)));
}
}
problems
}
pub fn check_flow(graph: &PipelineGraph) -> Vec<Problem> {
match crate::pipeline::engines::basic::validate_flow(graph) {
Ok(()) => Vec::new(),
Err(err) => vec![Problem::pipeline(err.message)],
}
}
pub fn check_pipeline(
graph: &PipelineGraph,
defs: &[NodeDefinition],
catalogue: Catalogue,
credential_kind: &dyn Fn(&str) -> Option<String>,
) -> PipelineCheck {
let mut refusals = check_graph_nodes(graph, defs, catalogue, credential_kind);
refusals.extend(check_flow(graph));
let mut warnings = references::check_references(graph, defs);
warnings.extend(answers::unanswered_routes(graph));
PipelineCheck { refusals, warnings }
}
pub fn no_credentials(_: &str) -> Option<String> {
None
}
#[cfg(test)]
mod tests;