use crate::metadata::{ParameterDef, ParameterType};
use crate::pipeline::PipelineNode;
use crate::processor::NodeProcessor;
type Params = serde_json::Map<String, serde_json::Value>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunTimeParam {
pub node_id: String,
pub node_type: String,
pub param: String,
}
impl std::fmt::Display for RunTimeParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"node \"{}\" ({}), param \"{}\": left for run time",
self.node_id, self.node_type, self.param
)
}
}
#[derive(Default)]
pub(super) struct LeftOut {
pub problems: Vec<String>,
pub givable: Vec<String>,
pub blocked: Vec<(String, String)>,
}
pub(super) struct Subject<'a> {
pub node: &'a PipelineNode,
pub processor: &'a dyn NodeProcessor,
pub params: &'a Params,
}
pub(super) fn left_out(
subject: &Subject<'_>,
written: Vec<String>,
root: &[PipelineNode],
) -> LeftOut {
let mut left = LeftOut::default();
let mut given = subject.params.clone();
let declared = subject.processor.metadata().parameters;
let absent = declared
.iter()
.filter(|d| !subject.params.contains_key(&d.name));
for def in absent.filter(|d| is_needed(subject, d, &written)) {
match unreachable_reason(subject.node, def, root) {
Some(reason) => left.blocked.push((def.name.clone(), reason)),
None => {
given.insert(def.name.clone(), stand_in(def));
left.givable.push(def.name.clone());
}
}
}
let after = subject.processor.validate(&given);
left.problems = written.into_iter().filter(|m| after.contains(m)).collect();
left
}
fn is_needed(subject: &Subject<'_>, def: &ParameterDef, written: &[String]) -> bool {
let mut given = subject.params.clone();
given.insert(def.name.clone(), stand_in(def));
let after = subject.processor.validate(&given);
written.iter().any(|m| !after.contains(m))
}
fn unreachable_reason(
node: &PipelineNode,
def: &ParameterDef,
root: &[PipelineNode],
) -> Option<String> {
if let Some(reason) = id_reason(&node.id) {
return Some(reason);
}
if !first_with_id(root, &node.id).is_some_and(|first| std::ptr::eq(first, node)) {
return Some(format!(
"-p {}:… sets an earlier node with the same id",
node.id
));
}
if node.fields.contains_key(&def.name) {
return Some(format!(
"-p {}:{}=… sets the node's field of that name instead",
node.id, def.name
));
}
type_reason(&def.param_type)
}
fn id_reason(id: &str) -> Option<String> {
if id.is_empty() {
return Some("-p can't name a node with an empty id".to_string());
}
id.chars()
.find(|c| matches!(c, ':' | '='))
.map(|c| format!("-p can't name a node whose id holds '{c}'"))
}
fn first_with_id<'a>(nodes: &'a [PipelineNode], id: &str) -> Option<&'a PipelineNode> {
nodes.iter().find_map(|node| {
if node.id == id {
return Some(node);
}
first_with_id(node.children.as_deref().unwrap_or_default(), id)
})
}
fn type_reason(param_type: &ParameterType) -> Option<String> {
matches!(param_type, ParameterType::Object)
.then(|| "-p gives text, numbers or true/false, not an object".to_string())
}
fn stand_in(def: &ParameterDef) -> serde_json::Value {
let text = || {
def.placeholder
.clone()
.unwrap_or_else(|| "run-time".to_string())
};
match &def.param_type {
ParameterType::Number => def
.constraints
.as_ref()
.and_then(|c| c.min)
.unwrap_or(0.0)
.into(),
ParameterType::Boolean => false.into(),
ParameterType::Enum { options } => options
.first()
.map_or_else(text, |o| o.value.clone())
.into(),
ParameterType::Object => serde_json::Value::Object(Params::new()),
ParameterType::String | ParameterType::File { .. } => text().into(),
}
}
#[cfg(test)]
mod tests;