use std::collections::{BTreeMap, BTreeSet, HashMap};
use serde_json::Value;
use super::Problem;
use super::suggest::did_you_mean;
use crate::pipeline::model::{PipelineGraph, PipelineNode};
use crate::pipeline::nodes::answer_key;
use crate::pipeline::nodes::basic::logic;
use crate::pipeline::NodeDefinition;
const ERROR_PIN: &str = "error";
#[derive(Debug, Clone, Default)]
struct Keys {
keys: BTreeMap<String, BTreeSet<usize>>,
open: bool,
failed: BTreeSet<usize>,
}
impl Keys {
fn merge(&mut self, other: &Keys) {
self.open |= other.open;
for (key, from) in &other.keys {
self.keys.entry(key.clone()).or_default().extend(from);
}
}
}
fn adds(node: &PipelineNode, def: Option<&NodeDefinition>) -> Result<Option<String>, ()> {
let kind = node.kind.as_str();
if def.is_none() && !kind.starts_with("x.") {
return Err(());
}
if kind.starts_with("input.") {
return match node.config.get("name").and_then(Value::as_str).map(str::trim) {
Some(name) if !name.is_empty() && !name.contains("{{") => Ok(Some(name.to_string())),
_ => Err(()),
};
}
match answer_key(kind) {
Some(key) => Ok(Some(key)),
None if kind.starts_with("logic.") || kind == "web.response.send" => Ok(None),
None => Err(()),
}
}
fn failure_key_of(node: &PipelineNode) -> String {
crate::pipeline::nodes::failure_key(&node.kind, node.config.get("name").and_then(Value::as_str))
}
struct Read {
flag: String,
text: String,
node: Option<String>,
key: String,
}
fn reads_in(text: &str) -> Vec<(String, Option<String>, String)> {
static READS: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let re = READS.get_or_init(|| {
regex::Regex::new(r"(\$nodes\s*\??\.\s*([A-Za-z_][\w]*)|input)\s*\??\.\s*([A-Za-z_$][\w$]*)").expect("reads")
});
let mut out = Vec::new();
for caps in re.captures_iter(text) {
let whole = caps.get(0).expect("match");
let before = text[..whole.start()].chars().last();
if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '$') {
continue;
}
out.push((whole.as_str().to_string(), caps.get(2).map(|m| m.as_str().to_string()), caps[3].to_string()));
}
out
}
fn expressions(text: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut rest = text;
while let Some(start) = rest.find("{{") {
let after = &rest[start + 2..];
let Some(end) = after.find("}}") else { break };
out.push(&after[..end]);
rest = &after[end + 2..];
}
out
}
fn collect_strings<'a>(value: &'a Value, out: &mut Vec<&'a str>) {
match value {
Value::String(s) => out.push(s),
Value::Array(items) => items.iter().for_each(|v| collect_strings(v, out)),
Value::Object(map) => map.values().for_each(|v| collect_strings(v, out)),
_ => {}
}
}
fn reads(node: &PipelineNode, def: Option<&NodeDefinition>) -> Vec<Read> {
let Some(map) = node.config.as_object() else { return Vec::new() };
let mut out = Vec::new();
for (key, value) in map {
if matches!(key.as_str(), "ui" | "preview" | "title") {
continue;
}
let flag = def.and_then(|d| d.dsl_flags.iter().find(|f| &f.config_key == key));
let name = flag.map(|f| f.flag.clone()).unwrap_or_else(|| format!("--{}", key.replace('_', "-")));
let whole = flag.is_some_and(|f| f.value == "expression");
let mut strings = Vec::new();
collect_strings(value, &mut strings);
for s in strings {
let spans = if whole && !s.contains("{{") { vec![s] } else { expressions(s) };
for span in spans {
for (text, node_id, read_key) in reads_in(span) {
out.push(Read { flag: name.clone(), text, node: node_id, key: read_key });
}
}
}
}
out
}
pub fn check_references(graph: &PipelineGraph, defs: &[NodeDefinition]) -> Vec<Problem> {
let Ok(loops) = crate::pipeline::engines::basic::flow_loops(graph) else { return Vec::new() };
let n = graph.nodes.len();
let index: HashMap<&str, usize> = graph.nodes.iter().enumerate().map(|(i, node)| (node.id.as_str(), i)).collect();
let def_of: Vec<Option<&NodeDefinition>> =
graph.nodes.iter().map(|node| defs.iter().find(|d| d.kind == node.kind)).collect();
let mut incoming: Vec<Vec<(usize, &str)>> = vec![Vec::new(); n];
for edge in &graph.edges {
let (Some(&from), Some(&to)) = (index.get(edge.from_node.as_str()), index.get(edge.to_node.as_str())) else {
continue;
};
if graph.nodes[from].kind == logic::retry::NODE_KIND && edge.from_pin == logic::retry::OUTPUT_PIN_RETRY {
continue;
}
incoming[to].push((from, edge.from_pin.as_str()));
}
let Some(order) = topological(n, &incoming) else { return Vec::new() };
let mut keys_in: Vec<Keys> = vec![Keys::default(); n];
let mut keys_out: Vec<Keys> = vec![Keys::default(); n];
for &i in &order {
let node = &graph.nodes[i];
let mut payload = Keys::default();
if let Some(foreach) = loops.closes.get(&node.id).and_then(|f| index.get(f.as_str())) {
payload = keys_in[*foreach].clone();
} else if incoming[i].is_empty() {
payload.open = !node.kind.starts_with("trigger.");
} else {
let mut failed: Option<BTreeSet<usize>> = None;
let mut meet = |set: &BTreeSet<usize>| {
failed = Some(match failed.take() {
None => set.clone(),
Some(have) => have.intersection(set).copied().collect(),
});
};
for &(from, pin) in &incoming[i] {
if graph.nodes[from].kind == logic::foreach_::NODE_KIND && pin == logic::foreach_::OUTPUT_PIN_ITEM {
let mut item = Keys::default();
for key in ["item", "index", "count"] {
item.keys.entry(key.to_string()).or_default().insert(from);
}
if graph.nodes[from].config.get("keep_input").and_then(Value::as_bool).unwrap_or(false) {
item.merge(&keys_in[from]);
}
meet(&keys_in[from].failed);
payload.merge(&item);
} else if pin == ERROR_PIN {
let mut failure = keys_in[from].clone();
failure.keys.insert(failure_key_of(&graph.nodes[from]), BTreeSet::from([from]));
failure.failed.insert(from);
meet(&failure.failed);
payload.merge(&failure);
} else {
meet(&keys_out[from].failed);
payload.merge(&keys_out[from]);
}
}
payload.failed = failed.unwrap_or_default();
}
let mut out = payload.clone();
match adds(node, def_of[i]) {
Ok(Some(key)) => {
out.keys.entry(key).or_default().insert(i);
}
Ok(None) => {}
Err(()) => out.open = true,
}
keys_in[i] = payload;
keys_out[i] = out;
}
let mut problems = Vec::new();
for &i in &order {
let node = &graph.nodes[i];
for read in reads(node, def_of[i]) {
if read.key.starts_with("__") {
continue;
}
let message = match &read.node {
None => input_problem(graph, &keys_in[i], i, &incoming[i], &def_of, &read),
Some(id) => {
let Some(&at) = index.get(id.as_str()) else { continue };
if keys_in[i].failed.contains(&at) {
Some(failed_node_problem(graph, at, &read))
} else {
nodes_problem(graph, &keys_out[at], at, &read)
}
}
};
if let Some(message) = message {
let problem = Problem::node(&node.id, &node.kind, Some(&read.flag), message);
if !problems.contains(&problem) {
problems.push(problem);
}
}
}
}
problems
}
fn input_problem(
graph: &PipelineGraph,
payload: &Keys,
at: usize,
incoming: &[(usize, &str)],
def_of: &[Option<&NodeDefinition>],
read: &Read,
) -> Option<String> {
let id = graph.nodes[at].id.as_str();
let key = read.key.as_str();
if let Some(from) = payload.keys.get(key) {
if from.len() < 2 {
return None;
}
let paths: Vec<String> = from.iter().map(|&n| format!("`$nodes.{}.{key}`", graph.nodes[n].id)).collect();
return Some(format!(
"`{}` — {} upstream nodes answer `{key}` ({}); `input.{key}` holds the one later in the text. Name the one you mean with `$nodes`",
read.text,
from.len(),
paths.join(" and ")
));
}
if payload.open {
return None;
}
let mut message = format!("`{}` — no node upstream of `{id}` answers `{key}`", read.text);
if let Some(better) = suggest_failure(graph, incoming, key) {
message.push_str(&format!("; a failure arrives under the failing node's key — did you mean `{better}`?"));
} else if let Some(better) = suggest_input(payload, incoming, def_of, key) {
message.push_str(&format!("; did you mean `{better}`?"));
} else if payload.keys.is_empty() {
message.push_str("; its input is empty");
}
Some(message)
}
fn suggest_failure(graph: &PipelineGraph, incoming: &[(usize, &str)], key: &str) -> Option<String> {
if key != "error" {
return None;
}
let from = incoming.iter().rev().find(|(_, pin)| *pin == ERROR_PIN).map(|&(from, _)| from)?;
Some(format!("input.{}.error", failure_key_of(&graph.nodes[from])))
}
fn suggest_input(
payload: &Keys,
incoming: &[(usize, &str)],
def_of: &[Option<&NodeDefinition>],
key: &str,
) -> Option<String> {
let mut providers: Vec<(usize, &str)> =
payload.keys.iter().flat_map(|(k, from)| from.iter().map(move |&n| (n, k.as_str()))).collect();
providers.sort_by(|a, b| b.0.cmp(&a.0));
for &(n, answer) in &providers {
let nested = def_of[n]
.and_then(|def| def.output_schema.pointer(&format!("/properties/{answer}/properties/{key}")))
.is_some();
if nested {
return Some(format!("input.{answer}.{key}"));
}
}
let names: Vec<&str> = payload.keys.keys().map(String::as_str).collect();
if let Some(near) = did_you_mean(key, &names) {
return Some(format!("input.{near}"));
}
let feeder = incoming.iter().map(|&(from, _)| from).max()?;
providers
.iter()
.find(|(n, _)| *n == feeder)
.or_else(|| providers.first())
.map(|(_, answer)| format!("input.{answer}"))
}
fn failed_node_problem(graph: &PipelineGraph, at: usize, read: &Read) -> String {
let id = graph.nodes[at].id.as_str();
let key = failure_key_of(&graph.nodes[at]);
let mut message = format!("`{}` — on the `:error` path of `{id}`, `$nodes.{id}` is null; read `input.{key}.error`", read.text);
if read.key == key {
message.push_str(&format!(" — did you mean `input.{key}`?"));
}
message
}
fn nodes_problem(graph: &PipelineGraph, answered: &Keys, at: usize, read: &Read) -> Option<String> {
let key = read.key.as_str();
if answered.open || answered.keys.contains_key(key) {
return None;
}
let id = graph.nodes[at].id.as_str();
let own: Option<&str> = answered.keys.iter().filter(|(_, from)| from.contains(&at)).map(|(k, _)| k.as_str()).next();
let names: Vec<&str> = answered.keys.keys().map(String::as_str).collect();
let better = did_you_mean(key, &names)
.map(|near| format!("$nodes.{id}.{near}"))
.or_else(|| own.map(|own| format!("$nodes.{id}.{own}.{key}")));
let mut message = format!("`{}` — `{id}` answers no `{key}`", read.text);
if let Some(own) = own {
message.push_str(&format!(" (its answer is `{own}`)"));
}
if let Some(better) = better {
message.push_str(&format!("; did you mean `{better}`?"));
}
Some(message)
}
fn topological(n: usize, incoming: &[Vec<(usize, &str)>]) -> Option<Vec<usize>> {
let mut remaining: Vec<usize> = incoming.iter().map(Vec::len).collect();
let mut outgoing: Vec<Vec<usize>> = vec![Vec::new(); n];
for (to, list) in incoming.iter().enumerate() {
for &(from, _) in list {
outgoing[from].push(to);
}
}
let mut ready: BTreeSet<usize> = (0..n).filter(|&i| remaining[i] == 0).collect();
let mut order = Vec::with_capacity(n);
while let Some(i) = ready.pop_first() {
order.push(i);
for &to in &outgoing[i] {
remaining[to] -= 1;
if remaining[to] == 0 {
ready.insert(to);
}
}
}
(order.len() == n).then_some(order)
}