use crate::check::{CheckReport, RunTimeParam, check_pipeline};
use crate::context::ProcessContext;
use crate::errors::BntoError;
use crate::metadata::{Constraints, NodeCategory, NodeMetadata, ParameterDef, ParameterType};
use crate::pipeline::PipelineDefinition;
use crate::processor::{NodeInput, NodeOutput, NodeProcessor};
use crate::progress::ProgressReporter;
use crate::registry::NodeRegistry;
use serde_json::{Value, json};
struct Needy;
impl NodeProcessor for Needy {
fn name(&self) -> &str {
"needy"
}
fn validate(&self, params: &serde_json::Map<String, Value>) -> Vec<String> {
let mut errors = Vec::new();
if params
.get("base")
.and_then(Value::as_str)
.is_none_or(str::is_empty)
{
errors.push("'base' is required".to_string());
}
if !params.get("values").is_some_and(Value::is_object) {
errors.push("'values' is required".to_string());
}
if let Some(mode) = params.get("mode").and_then(Value::as_str)
&& mode != "a"
{
errors.push(format!("mode must be \"a\", got \"{mode}\""));
}
errors
}
fn metadata(&self) -> NodeMetadata {
NodeMetadata {
node_type: "needy".to_string(),
name: "Needy".to_string(),
description: String::new(),
category: NodeCategory::Data,
accepts: vec![],
platforms: vec![],
parameters: vec![
required("base", ParameterType::String),
required("values", ParameterType::Object),
ParameterDef {
name: "mode".to_string(),
..Default::default()
},
],
input_cardinality: Default::default(),
requires: vec![],
}
}
fn process(
&self,
_input: NodeInput,
_progress: &ProgressReporter,
_ctx: &dyn ProcessContext,
) -> Result<NodeOutput, BntoError> {
unreachable!("check never runs a node")
}
}
fn required(name: &str, param_type: ParameterType) -> ParameterDef {
ParameterDef {
name: name.to_string(),
param_type,
constraints: Some(Constraints {
min: None,
max: None,
required: true,
}),
..Default::default()
}
}
fn check(def: Value) -> CheckReport {
let definition: PipelineDefinition = serde_json::from_value(def).unwrap();
let mut registry = NodeRegistry::new();
registry.register("needy", Box::new(Needy));
check_pipeline(&definition, ®istry)
}
fn needy(id: &str, mut params: Value) -> Value {
params["values"] = json!({ "k": "v" });
json!({ "id": id, "type": "needy", "parameters": params })
}
fn note(node_id: &str, param: &str) -> RunTimeParam {
RunTimeParam {
node_id: node_id.to_string(),
node_type: "needy".to_string(),
param: param.to_string(),
}
}
#[test]
fn test_a_left_out_text_param_is_left_for_run_time() {
let report = check(json!({ "nodes": [needy("clean", json!({}))] }));
assert_eq!(report.findings, Vec::new());
assert_eq!(report.run_time, vec![note("clean", "base")]);
}
#[test]
fn test_other_problems_on_the_node_still_fail() {
let report = check(json!({ "nodes": [needy("clean", json!({ "mode": "z" }))] }));
assert_eq!(report.findings.len(), 1, "{:?}", report.findings);
assert!(report.findings[0].message.contains("mode must be"));
assert_eq!(report.run_time, vec![note("clean", "base")]);
}
#[test]
fn test_a_param_inside_a_loop_is_left_for_run_time() {
let report = check(json!({ "nodes": [
{ "id": "each", "type": "loop", "nodes": [
{ "id": "group", "type": "group", "nodes": [needy("clean", json!({}))] }
] }
] }));
assert_eq!(report.findings, Vec::new());
assert_eq!(report.run_time, vec![note("clean", "base")]);
}
#[test]
fn test_a_sound_recipe_leaves_nothing_for_run_time() {
let report = check(json!({ "nodes": [needy("clean", json!({ "base": "." }))] }));
assert_eq!(report, CheckReport::default());
}
#[test]
fn test_an_empty_value_is_not_left_out() {
let report = check(json!({ "nodes": [needy("clean", json!({ "base": "" }))] }));
assert_eq!(report.findings.len(), 1, "{:?}", report.findings);
assert_eq!(report.findings[0].message, "'base' is required");
assert_eq!(report.run_time, Vec::new());
}
#[test]
fn test_a_left_out_object_param_fails() {
let report = check(json!({ "nodes": [
{ "id": "set", "type": "needy", "parameters": { "base": "." } }
] }));
let messages: Vec<&str> = report.findings.iter().map(|f| f.message.as_str()).collect();
assert_eq!(messages.len(), 2, "{messages:?}");
assert_eq!(messages[0], "'values' is required");
assert!(messages[1].contains("no run can give it"), "{messages:?}");
assert!(messages[1].contains("object"), "{messages:?}");
assert_eq!(report.findings[1].param.as_deref(), Some("values"));
assert_eq!(report.run_time, Vec::new());
}
#[test]
fn test_a_node_id_with_a_colon_fails() {
let report = check(json!({ "nodes": [needy("clean:up", json!({}))] }));
assert_eq!(report.findings.len(), 2, "{:?}", report.findings);
assert_eq!(report.findings[0].message, "'base' is required");
assert!(
report.findings[1].message.contains("':'"),
"{:?}",
report.findings
);
assert_eq!(report.run_time, Vec::new());
}
#[test]
fn test_a_node_id_with_an_equals_sign_fails() {
let report = check(json!({ "nodes": [needy("a=b", json!({}))] }));
assert_eq!(report.findings.len(), 2, "{:?}", report.findings);
assert_eq!(report.run_time, Vec::new());
}
#[test]
fn test_a_node_behind_an_earlier_one_with_its_id_fails() {
let report = check(json!({ "nodes": [
{ "id": "each", "type": "loop", "nodes": [needy("clean", json!({}))] },
needy("clean", json!({}))
] }));
assert_eq!(report.run_time, vec![note("clean", "base")]);
assert_eq!(report.findings.len(), 2, "{:?}", report.findings);
assert_eq!(report.findings[0].message, "'base' is required");
assert!(
report.findings[1].message.contains("earlier node"),
"{:?}",
report.findings
);
}
#[test]
fn test_a_param_named_like_a_declared_field_fails() {
let mut node = needy("clean", json!({}));
node["fields"] = json!({ "base": { "type": "string", "label": "Base" } });
let report = check(json!({ "nodes": [node] }));
assert_eq!(report.findings.len(), 2, "{:?}", report.findings);
assert!(
report.findings[1].message.contains("field"),
"{:?}",
report.findings
);
assert_eq!(report.run_time, Vec::new());
}
#[test]
fn test_run_time_param_reads_as_node_and_param() {
assert_eq!(
note("clean", "base").to_string(),
"node \"clean\" (needy), param \"base\": left for run time"
);
}