Skip to main content

osp_cli/dsl/stages/
question.rs

1use crate::core::{
2    output_model::{Group, OutputItems},
3    row::Row,
4};
5use anyhow::Result;
6use serde_json::Value;
7
8use crate::dsl::stages::quick;
9
10pub fn apply(items: OutputItems, spec: &str) -> Result<OutputItems> {
11    let trimmed = spec.trim();
12    if trimmed.is_empty() {
13        return Ok(clean_items(items));
14    }
15
16    let raw = format!("?{trimmed}");
17    let out = match items {
18        OutputItems::Rows(rows) => OutputItems::Rows(quick::apply(rows, &raw)?),
19        OutputItems::Groups(groups) => OutputItems::Groups(apply_groups_quick(groups, &raw)?),
20    };
21    Ok(out)
22}
23
24fn clean_items(items: OutputItems) -> OutputItems {
25    match items {
26        OutputItems::Rows(rows) => OutputItems::Rows(clean_rows(rows)),
27        OutputItems::Groups(groups) => OutputItems::Groups(
28            groups
29                .into_iter()
30                .map(|group| Group {
31                    groups: group.groups,
32                    aggregates: group.aggregates,
33                    rows: clean_rows(group.rows),
34                })
35                .collect(),
36        ),
37    }
38}
39
40fn clean_rows(rows: Vec<Row>) -> Vec<Row> {
41    rows.into_iter().filter_map(clean_row).collect()
42}
43
44pub(crate) fn clean_row(row: Row) -> Option<Row> {
45    let cleaned = row
46        .into_iter()
47        .filter(|(_, value)| !is_empty_value(value))
48        .collect::<Row>();
49    if cleaned.is_empty() {
50        None
51    } else {
52        Some(cleaned)
53    }
54}
55
56fn is_empty_value(value: &Value) -> bool {
57    match value {
58        Value::Null => true,
59        Value::String(text) => text.is_empty(),
60        Value::Array(items) => items.is_empty(),
61        _ => false,
62    }
63}
64
65fn apply_groups_quick(groups: Vec<Group>, raw: &str) -> Result<Vec<Group>> {
66    let mut out = Vec::with_capacity(groups.len());
67    for group in groups {
68        let rows = quick::apply(group.rows, raw)?;
69        out.push(Group {
70            groups: group.groups,
71            aggregates: group.aggregates,
72            rows,
73        });
74    }
75    Ok(out)
76}
77
78#[cfg(test)]
79mod tests {
80    use crate::core::output_model::{Group, OutputItems};
81    use serde_json::json;
82
83    use super::apply;
84
85    fn row(value: serde_json::Value) -> crate::core::row::Row {
86        value
87            .as_object()
88            .cloned()
89            .expect("fixture should be an object")
90    }
91
92    #[test]
93    fn empty_spec_cleans_rows_and_drops_empty_results() {
94        let items = OutputItems::Rows(vec![
95            row(json!({"uid": "oistes", "mail": "", "tags": [], "note": null})),
96            row(json!({"mail": "", "tags": [], "note": null})),
97        ]);
98
99        let cleaned = apply(items, "   ").expect("cleaning should succeed");
100        let OutputItems::Rows(rows) = cleaned else {
101            panic!("expected row output");
102        };
103
104        assert_eq!(rows.len(), 1);
105        assert_eq!(rows[0].len(), 1);
106        assert_eq!(
107            rows[0].get("uid").and_then(|value| value.as_str()),
108            Some("oistes")
109        );
110    }
111
112    #[test]
113    fn empty_spec_cleans_group_rows_without_touching_group_metadata() {
114        let items = OutputItems::Groups(vec![Group {
115            groups: row(json!({"team": "ops"})),
116            aggregates: row(json!({"count": 2})),
117            rows: vec![
118                row(json!({"uid": "oistes", "mail": ""})),
119                row(json!({"mail": "", "tags": []})),
120            ],
121        }]);
122
123        let cleaned = apply(items, "").expect("group cleaning should succeed");
124        let OutputItems::Groups(groups) = cleaned else {
125            panic!("expected grouped output");
126        };
127
128        assert_eq!(groups.len(), 1);
129        assert_eq!(
130            groups[0]
131                .groups
132                .get("team")
133                .and_then(|value| value.as_str()),
134            Some("ops")
135        );
136        assert_eq!(
137            groups[0]
138                .aggregates
139                .get("count")
140                .and_then(|value| value.as_i64()),
141            Some(2)
142        );
143        assert_eq!(groups[0].rows.len(), 1);
144        assert_eq!(groups[0].rows[0].len(), 1);
145        assert_eq!(
146            groups[0].rows[0]
147                .get("uid")
148                .and_then(|value| value.as_str()),
149            Some("oistes")
150        );
151    }
152
153    #[test]
154    fn non_empty_spec_reuses_quick_filter_for_rows_and_groups() {
155        let rows = OutputItems::Rows(vec![
156            row(json!({"uid": "oistes"})),
157            row(json!({"mail": "other@example.org"})),
158        ]);
159        let filtered = apply(rows, "uid").expect("row filter should succeed");
160        let OutputItems::Rows(rows) = filtered else {
161            panic!("expected row output");
162        };
163        assert_eq!(rows.len(), 1);
164        assert_eq!(
165            rows[0].get("uid").and_then(|value| value.as_str()),
166            Some("oistes")
167        );
168
169        let groups = OutputItems::Groups(vec![Group {
170            groups: row(json!({"team": "ops"})),
171            aggregates: row(json!({"count": 2})),
172            rows: vec![
173                row(json!({"uid": "oistes"})),
174                row(json!({"mail": "other@example.org"})),
175            ],
176        }]);
177        let filtered = apply(groups, "uid").expect("group filter should succeed");
178        let OutputItems::Groups(groups) = filtered else {
179            panic!("expected grouped output");
180        };
181        assert_eq!(groups[0].rows.len(), 1);
182        assert_eq!(
183            groups[0].rows[0]
184                .get("uid")
185                .and_then(|value| value.as_str()),
186            Some("oistes")
187        );
188    }
189}