Skip to main content

bnto_core/control/report/
mod.rs

1// report — Print a templated line to the run's output.
2//
3// A generic control node, like NiFi's LogMessage or a Node-RED debug node:
4// prints `message` and passes its files on unchanged. It never fails the run
5// on what it prints, only on a recipe mistake (no message, an `each` that
6// isn't a list, a placeholder key that isn't there).
7//
8// It runs as a batch so it prints even when no files arrive. With `each`
9// (a list, usually a sole `{{node.<id>.<key>}}`) it prints a line per entry,
10// in order; else, when `message` reads `{{file}}` or `{{item.*}}`, a line
11// per incoming file; else one line. A dry run prints what it would report.
12
13mod lines;
14mod params;
15
16use crate::metadata::{InputCardinality, NodeCategory, NodeMetadata};
17use crate::processor::{BatchFile, BatchInput, NodeInput, NodeOutput, NodeProcessor};
18use crate::progress::ProgressReporter;
19use crate::{BntoError, ProcessContext};
20
21use super::pass_through::pass_through;
22use params::{ReportParams, build_report_parameters};
23
24/// Report processor — prints a templated line without failing the run.
25pub struct Report;
26
27impl Report {
28    pub fn new() -> Self {
29        Self
30    }
31}
32
33impl Default for Report {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl NodeProcessor for Report {
40    fn name(&self) -> &str {
41        "report"
42    }
43
44    fn metadata(&self) -> NodeMetadata {
45        NodeMetadata {
46            node_type: "report".to_string(),
47            name: "Report".to_string(),
48            description: "Report a templated line in the run's output: once, once per file, \
49                          or once per entry of a list; files pass through unchanged."
50                .to_string(),
51            category: NodeCategory::Control,
52            accepts: vec![],
53            platforms: vec![
54                "browser".to_string(),
55                "cli".to_string(),
56                "desktop".to_string(),
57            ],
58            parameters: build_report_parameters(),
59            input_cardinality: InputCardinality::Batch,
60            requires: vec![],
61        }
62    }
63
64    fn validate(&self, params: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
65        ReportParams::read(params).err().into_iter().collect()
66    }
67
68    fn resolves_items(&self) -> bool {
69        true
70    }
71
72    /// Required by the trait; the executor always runs this node as a batch.
73    fn process(
74        &self,
75        input: NodeInput,
76        progress: &ProgressReporter,
77        ctx: &dyn ProcessContext,
78    ) -> Result<NodeOutput, BntoError> {
79        let file = BatchFile {
80            data: input.data,
81            filename: input.filename,
82            mime_type: input.mime_type,
83            metadata: serde_json::Map::new(),
84        };
85        let batch = BatchInput {
86            files: vec![file],
87            params: input.params,
88        };
89        self.process_batch(batch, progress, ctx)
90    }
91
92    fn process_batch(
93        &self,
94        input: BatchInput,
95        progress: &ProgressReporter,
96        ctx: &dyn ProcessContext,
97    ) -> Result<NodeOutput, BntoError> {
98        let params = ReportParams::read(&input.params).map_err(BntoError::InvalidInput)?;
99        if ctx.dry_run() {
100            progress.report_output(&format!("Would report: {}", params.message));
101            return Ok(pass_through(input.files));
102        }
103        // Fill every line before printing any, so a refusal prints nothing.
104        let lines = lines::lines(&params, &input.files).map_err(BntoError::InvalidInput)?;
105        for line in &lines {
106            progress.report_output(line);
107        }
108        Ok(pass_through(input.files))
109    }
110}
111
112#[cfg(test)]
113mod tests;