use serde_json::Value;
const NAMESPACES: [&str; 7] = ["fields", "env", "ctx", "node", "item", "vars", "entry"];
const OPEN: &str = "{{";
const CLOSE: &str = "}}";
pub(super) fn value_problems(value: &Value) -> Vec<String> {
match value {
Value::String(s) => string_problems(s),
Value::Array(items) => items.iter().flat_map(value_problems).collect(),
Value::Object(map) => map.values().flat_map(value_problems).collect(),
_ => Vec::new(),
}
}
fn string_problems(s: &str) -> Vec<String> {
let mut problems = Vec::new();
let mut rest = s;
while let Some(start) = rest.find(OPEN) {
let (problem, remaining) = scan_placeholder(&rest[start + OPEN.len()..]);
problems.extend(problem);
rest = remaining;
}
problems
}
fn scan_placeholder(after: &str) -> (Option<String>, &str) {
let next_open = after.find(OPEN);
match after.find(CLOSE) {
Some(end) if next_open.is_none_or(|open| open > end) => (
namespace_problem(&after[..end]),
&after[end + CLOSE.len()..],
),
_ => {
let stop = next_open.unwrap_or(after.len());
(Some(unclosed(&after[..stop])), &after[stop..])
}
}
}
const QUOTE_CHARS: usize = 40;
fn unclosed(text: &str) -> String {
let quoted: String = text.trim_end().chars().take(QUOTE_CHARS).collect();
format!("unclosed placeholder \"{OPEN}{quoted}\"")
}
fn namespace_problem(inner: &str) -> Option<String> {
let (namespace, _) = inner.split_once('.')?;
if namespace.trim().is_empty() || NAMESPACES.contains(&namespace) {
return None;
}
Some(format!(
"unknown placeholder namespace \"{namespace}\" in \"{OPEN}{inner}{CLOSE}\" (known: {})",
NAMESPACES.join(", ")
))
}