use super::*;
struct TagProcessor;
impl crate::processor::NodeProcessor for TagProcessor {
fn name(&self) -> &str {
"tag"
}
fn process(
&self,
input: NodeInput,
_progress: &ProgressReporter,
_ctx: &dyn ProcessContext,
) -> Result<NodeOutput, BntoError> {
let mut metadata = serde_json::Map::new();
metadata.insert(
"new_name".to_string(),
format!("renamed-{}", input.filename).into(),
);
if let Some(tag) = input.params.get("tag") {
metadata.insert("tag".to_string(), tag.clone());
}
Ok(NodeOutput {
files: vec![OutputFile {
data: input.data,
filename: input.filename,
mime_type: "application/octet-stream".to_string(),
metadata,
}],
metadata: serde_json::Map::new(),
})
}
}
struct NameFromParamProcessor;
impl crate::processor::NodeProcessor for NameFromParamProcessor {
fn name(&self) -> &str {
"name-from-param"
}
fn process(
&self,
input: NodeInput,
_progress: &ProgressReporter,
_ctx: &dyn ProcessContext,
) -> Result<NodeOutput, BntoError> {
let name = input.params["name"]
.as_str()
.unwrap_or_default()
.to_string();
Ok(NodeOutput {
files: vec![OutputFile {
data: input.data,
filename: name,
mime_type: "application/octet-stream".to_string(),
metadata: serde_json::Map::new(),
}],
metadata: serde_json::Map::new(),
})
}
}
pub(super) fn item_registry() -> NodeRegistry {
let mut registry = mock_registry();
registry.register("test-tag", Box::new(TagProcessor));
registry.register("test-name", Box::new(NameFromParamProcessor));
registry
}
fn file_with_metadata(name: &str, metadata: serde_json::Value) -> PipelineFile {
PipelineFile {
metadata: metadata.as_object().unwrap().clone(),
..make_file(name, b"data")
}
}
fn run(def_json: &str, files: Vec<PipelineFile>) -> Result<PipelineResult, BntoError> {
let def = parse_def(def_json);
let reporter = PipelineReporter::new_noop();
execute_pipeline(
&def,
files,
&item_registry(),
&reporter,
&NoopContext,
fake_now,
)
}
fn run_error(def_json: &str, files: Vec<PipelineFile>) -> String {
match run(def_json, files) {
Ok(result) => panic!(
"expected an error, got files {:?}",
result.files.iter().map(|f| &f.name).collect::<Vec<_>>()
),
Err(e) => e.to_string(),
}
}
pub(super) fn run_names(def_json: &str, files: Vec<PipelineFile>) -> Vec<String> {
let def = parse_def(def_json);
let reporter = PipelineReporter::new_noop();
let result = execute_pipeline(
&def,
files,
&item_registry(),
&reporter,
&NoopContext,
fake_now,
)
.unwrap();
result.files.into_iter().map(|f| f.name).collect()
}
#[test]
fn test_flat_recipe_resolves_item_from_each_files_metadata() {
let names = run_names(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a"), make_file("b.txt", b"b")],
);
assert_eq!(names, vec!["renamed-a.txt", "renamed-b.txt"]);
}
#[test]
fn test_flat_recipe_reads_metadata_the_input_file_carries() {
let names = run_names(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.row}}.txt" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![
file_with_metadata("a.txt", serde_json::json!({ "row": "first" })),
file_with_metadata("b.txt", serde_json::json!({ "row": "second" })),
],
);
assert_eq!(names, vec!["first.txt", "second.txt"]);
}
#[test]
fn test_item_in_a_mixed_template_string() {
let names = run_names(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "tag", "type": "test-tag", "parameters": { "tag": "x" } },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.tag}}_{{item.new_name}}" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a")],
);
assert_eq!(names, vec!["x_renamed-a.txt"]);
}
#[test]
fn test_inside_a_loop_a_node_sees_what_earlier_children_attached() {
let names = run_names(
r#"{
"nodes": [
{ "id": "in", "type": "input" },
{
"id": "loop-1", "type": "loop",
"children": [
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } }
]
},
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a")],
);
assert_eq!(names, vec!["renamed-a.txt"]);
}
#[test]
fn test_inside_a_loop_the_loop_item_stays_visible_under_file_metadata() {
let names = run_names(
r#"{
"nodes": [
{ "id": "in", "type": "input" },
{
"id": "loop-1", "type": "loop",
"children": [
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.row}}-{{item.new_name}}" } }
]
},
{ "id": "out", "type": "output" }
]
}"#,
vec![file_with_metadata(
"a.txt",
serde_json::json!({ "row": "r1" }),
)],
);
assert_eq!(names, vec!["r1-renamed-a.txt"]);
}
#[test]
fn test_missing_item_key_fails_naming_the_node_key_and_file() {
let err = run_error(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "x{{item.nope}}.txt" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("Pack/a.txt", b"a")],
);
assert!(err.contains("'name'"), "{err}");
assert!(err.contains("{{item.nope}}"), "{err}");
assert!(err.contains("Pack/a.txt"), "{err}");
}
#[test]
fn test_item_fails_when_the_file_has_no_metadata() {
let err = run_error(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a")],
);
assert!(
err.contains("{{item.new_name}}") && err.contains("a.txt"),
"{err}"
);
}
#[test]
fn test_one_unfilled_file_fails_the_whole_run() {
let err = run_error(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.row}}.txt" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![
file_with_metadata("a.txt", serde_json::json!({ "row": "first" })),
file_with_metadata("b.txt", serde_json::json!({ "other": "x" })),
],
);
assert!(
err.contains("{{item.row}}") && err.contains("b.txt"),
"{err}"
);
}
#[test]
fn test_params_without_item_templates_are_unaffected_by_missing_metadata() {
let names = run_names(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "name", "type": "test-name", "parameters": { "name": "fixed.txt" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a")],
);
assert_eq!(names, vec!["fixed.txt"]);
}
#[test]
fn test_inside_a_loop_the_loop_item_wins_a_shared_key() {
let names = run_names(
r#"{
"nodes": [
{ "id": "in", "type": "input" },
{
"id": "loop-1", "type": "loop",
"children": [
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } }
]
},
{ "id": "out", "type": "output" }
]
}"#,
vec![file_with_metadata(
"a.txt",
serde_json::json!({ "new_name": "from-row.txt" }),
)],
);
assert_eq!(names, vec!["from-row.txt"]);
}
#[test]
fn test_top_level_without_auto_iteration_resolves_per_file() {
let names = run_names(
r#"{
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "tag", "type": "test-tag" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"a"), make_file("b.txt", b"b")],
);
assert_eq!(names, vec!["renamed-a.txt", "renamed-b.txt"]);
}
#[test]
fn test_item_reads_what_any_earlier_node_attached() {
let names = run_names(
r#"{
"settings": { "iteration": "auto" },
"nodes": [
{ "id": "in", "type": "input" },
{ "id": "tag", "type": "test-tag" },
{ "id": "meta", "type": "test-metadata" },
{ "id": "name", "type": "test-name", "parameters": { "name": "{{item.new_name}}" } },
{ "id": "out", "type": "output" }
]
}"#,
vec![make_file("a.txt", b"aaaa")],
);
assert_eq!(names, vec!["renamed-a.txt"]);
}