Skip to main content

bnto_core/data/summarize/
mod.rs

1// summarize — Group a run's items by fields; each group reports its count and totals.
2//
3// A batch node, pure and browser-capable, like n8n's Summarize or a SQL
4// GROUP BY: `groupBy` names the fields whose values make a group, `sum` the
5// numeric fields each group totals. Out comes one item per group, in the
6// order its first item arrived, carrying only the group's values, `count` and
7// `sum_<field>`; it has no data, like spreadsheet-read's row items, and the
8// items summarized don't pass through. See `group.rs` and `total.rs`.
9
10mod group;
11mod params;
12mod total;
13
14use serde_json::{Map, Value};
15
16use crate::metadata::{InputCardinality, NodeCategory, NodeMetadata};
17use crate::processor::{
18    BatchFile, BatchInput, FileData, NodeInput, NodeOutput, NodeProcessor, OutputFile,
19};
20use crate::progress::ProgressReporter;
21use crate::{BntoError, ProcessContext};
22
23use params::{SummarizeParams, build_summarize_parameters};
24
25/// Summarize processor — one item out per group of items in.
26pub struct Summarize;
27
28impl Summarize {
29    pub fn new() -> Self {
30        Self
31    }
32}
33
34impl Default for Summarize {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl NodeProcessor for Summarize {
41    fn name(&self) -> &str {
42        "summarize"
43    }
44
45    fn metadata(&self) -> NodeMetadata {
46        NodeMetadata {
47            node_type: "summarize".to_string(),
48            name: "Summarize".to_string(),
49            description: "Summarize items by metadata fields, one item per group with \
50                          its count and the totals of numeric fields (sum_<field>). Groups \
51                          keep first-seen order; the items summarized don't pass through."
52                .to_string(),
53            category: NodeCategory::Data,
54            accepts: vec![],
55            platforms: vec![
56                "browser".to_string(),
57                "cli".to_string(),
58                "desktop".to_string(),
59            ],
60            parameters: build_summarize_parameters(),
61            input_cardinality: InputCardinality::Batch,
62            requires: vec![],
63        }
64    }
65
66    fn validate(&self, params: &Map<String, Value>) -> Vec<String> {
67        SummarizeParams::read(params).err().into_iter().collect()
68    }
69
70    /// Required by the trait; the executor always runs this node as a batch.
71    fn process(
72        &self,
73        input: NodeInput,
74        progress: &ProgressReporter,
75        ctx: &dyn ProcessContext,
76    ) -> Result<NodeOutput, BntoError> {
77        let file = BatchFile {
78            data: input.data,
79            filename: input.filename,
80            mime_type: input.mime_type,
81            metadata: Map::new(),
82        };
83        let batch = BatchInput {
84            files: vec![file],
85            params: input.params,
86        };
87        self.process_batch(batch, progress, ctx)
88    }
89
90    fn process_batch(
91        &self,
92        input: BatchInput,
93        _progress: &ProgressReporter,
94        _ctx: &dyn ProcessContext,
95    ) -> Result<NodeOutput, BntoError> {
96        let params = SummarizeParams::read(&input.params).map_err(BntoError::InvalidInput)?;
97        let groups = group::summarize(&input.files, &params).map_err(BntoError::InvalidInput)?;
98        let mut metadata = Map::new();
99        metadata.insert("groupCount".to_string(), groups.len().into());
100        let files = groups.into_iter().enumerate().map(summary_item).collect();
101        Ok(NodeOutput { files, metadata })
102    }
103}
104
105/// The `i`th group's item: no data, named `group{i}`, its fields as metadata.
106fn summary_item((i, metadata): (usize, Map<String, Value>)) -> OutputFile {
107    OutputFile {
108        data: FileData::Bytes(Vec::new()),
109        filename: format!("group{i}"),
110        mime_type: "application/octet-stream".to_string(),
111        metadata,
112    }
113}
114
115#[cfg(test)]
116mod tests;