Skip to main content

sheeter_engine/
transform.rs

1use crate::filter::Filter;
2use crate::CanonicalValue;
3
4/// Row-level transformations applied between reading and writing.
5///
6/// Transforms are applied in order: filter → rename → select → limit.
7/// All transforms are optional; an empty `Transforms` is a no-op.
8#[derive(Debug, Clone, Default)]
9pub struct Transforms {
10    pub filter: Option<Filter>,
11    pub select: Option<Vec<String>>,
12    pub rename: Vec<(String, String)>,
13    pub limit: Option<usize>,
14    pub dedup: Option<Vec<String>>,
15}
16
17impl Transforms {
18    pub fn is_empty(&self) -> bool {
19        self.filter.is_none()
20            && self.select.is_none()
21            && self.rename.is_empty()
22            && self.limit.is_none()
23            && self.dedup.is_none()
24    }
25
26    /// Applies transforms to a single row. Returns `None` if the row was
27    /// filtered out.
28    pub fn apply_to_row(
29        &self,
30        mut row: Vec<(String, CanonicalValue)>,
31    ) -> Option<Vec<(String, CanonicalValue)>> {
32        if let Some(f) = &self.filter {
33            let wrapped = CanonicalValue::Object(row.clone());
34            if !f.matches(&wrapped) {
35                return None;
36            }
37        }
38        for (from, to) in &self.rename {
39            for (k, _) in row.iter_mut() {
40                if k == from {
41                    *k = to.clone();
42                    break;
43                }
44            }
45        }
46        if let Some(sel) = &self.select {
47            let mut new_row = Vec::with_capacity(sel.len());
48            for name in sel {
49                match row.iter().find(|(k, _)| k == name) {
50                    Some((k, v)) => new_row.push((k.clone(), v.clone())),
51                    None => new_row.push((name.clone(), CanonicalValue::Null)),
52                }
53            }
54            row = new_row;
55        }
56        Some(row)
57    }
58
59    /// Applies transforms to a `CanonicalValue::Array` of objects.
60    pub fn apply_to_array(&self, value: &CanonicalValue) -> CanonicalValue {
61        if let CanonicalValue::Object(o) = value {
62            if o.len() == 1
63                && matches!(
64                    &o[0].1,
65                    CanonicalValue::Array(_) | CanonicalValue::Object(_)
66                )
67            {
68                let inner = self.apply_to_array(&o[0].1);
69                return CanonicalValue::Object(vec![(o[0].0.clone(), inner)]);
70            }
71        }
72        // Single row: object whose fields are all scalars. XML with one item
73        // and JSON with one root object both produce this shape.
74        if let CanonicalValue::Object(o) = value {
75            let all_scalar = o.iter().all(|(_, v)| {
76                matches!(
77                    v,
78                    CanonicalValue::Null
79                        | CanonicalValue::Bool(_)
80                        | CanonicalValue::Number(_)
81                        | CanonicalValue::String(_)
82                )
83            });
84            if all_scalar && !o.is_empty() {
85                if let Some(new_row) = self.apply_to_row(o.clone()) {
86                    return CanonicalValue::Object(new_row);
87                }
88                return CanonicalValue::Null;
89            }
90        }
91        let rows = match value {
92            CanonicalValue::Array(a) => a,
93            _ => return value.clone(),
94        };
95        let limit = self.limit.unwrap_or(usize::MAX);
96        let mut out = Vec::new();
97        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
98        for row in rows {
99            if out.len() >= limit {
100                break;
101            }
102            match row {
103                CanonicalValue::Object(o) => {
104                    if let Some(new_row) = self.apply_to_row(o.clone()) {
105                        if let Some(cols) = &self.dedup {
106                            let key: String = cols
107                                .iter()
108                                .map(|c| {
109                                    new_row
110                                        .iter()
111                                        .find(|(k, _)| k == c)
112                                        .map(|(_, v)| format!("{v:?}"))
113                                        .unwrap_or_default()
114                                })
115                                .collect::<Vec<_>>()
116                                .join("\x1f");
117                            if !seen.insert(key) {
118                                continue;
119                            }
120                        }
121                        out.push(CanonicalValue::Object(new_row));
122                    }
123                }
124                other => out.push(other.clone()),
125            }
126        }
127        CanonicalValue::Array(out)
128    }
129}