osp_cli/dsl/stages/
question.rs1use 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}