osp_cli/dsl/eval/
flatten.rs1use crate::core::row::Row;
2use serde_json::{Map, Value};
3
4use crate::dsl::parse::path::{Selector, parse_path};
5
6pub fn flatten_row(row: &Row) -> Row {
7 let mut out = Map::new();
8 for (key, value) in row {
9 flatten_value(Some(key.as_str()), value, &mut out);
10 }
11 out
12}
13
14pub fn flatten_rows(rows: &[Row]) -> Vec<Row> {
15 rows.iter().map(flatten_row).collect()
16}
17
18pub fn coalesce_flat_row(row: &Row) -> Row {
19 let mut root = Value::Object(Map::new());
20 for (key, value) in row {
21 let Ok(path) = parse_path(key) else {
22 continue;
23 };
24 let mut steps = Vec::new();
25 for segment in path.segments {
26 let Some(name) = segment.name else {
27 steps.clear();
28 break;
29 };
30 steps.push(Step::Key(name));
31 for selector in segment.selectors {
32 match selector {
33 Selector::Index(index) if index >= 0 => steps.push(Step::Index(index as usize)),
34 _ => {
35 steps.clear();
36 break;
37 }
38 }
39 }
40 }
41 if steps.is_empty() {
42 continue;
43 }
44 insert_value(&mut root, &steps, value.clone());
45 }
46
47 match root {
48 Value::Object(map) => map,
49 _ => Map::new(),
50 }
51}
52
53#[derive(Debug, Clone)]
54enum Step {
55 Key(String),
56 Index(usize),
57}
58
59fn insert_value(root: &mut Value, steps: &[Step], value: Value) {
60 if steps.is_empty() {
61 *root = value;
62 return;
63 }
64
65 let next_step = steps.get(1);
66 match &steps[0] {
67 Step::Key(key) => {
68 ensure_object(root);
69 if let Value::Object(map) = root {
70 let entry = map.entry(key.clone()).or_insert(Value::Null);
71 if steps.len() == 1 {
72 *entry = value;
73 return;
74 }
75 ensure_container(entry, next_step);
76 insert_value(entry, &steps[1..], value);
77 }
78 }
79 Step::Index(index) => {
80 ensure_array(root);
81 if let Value::Array(items) = root {
82 if items.len() <= *index {
83 items.resize(*index + 1, Value::Null);
84 }
85 let entry = &mut items[*index];
86 if steps.len() == 1 {
87 *entry = value;
88 return;
89 }
90 ensure_container(entry, next_step);
91 insert_value(entry, &steps[1..], value);
92 }
93 }
94 }
95}
96
97fn ensure_container(value: &mut Value, next_step: Option<&Step>) {
98 match next_step {
99 Some(Step::Key(_)) => ensure_object(value),
100 Some(Step::Index(_)) => ensure_array(value),
101 None => {}
102 }
103}
104
105fn ensure_object(value: &mut Value) {
106 if !value.is_object() {
107 *value = Value::Object(Map::new());
108 }
109}
110
111fn ensure_array(value: &mut Value) {
112 if !value.is_array() {
113 *value = Value::Array(Vec::new());
114 }
115}
116
117fn flatten_value(prefix: Option<&str>, value: &Value, out: &mut Row) {
118 match value {
119 Value::Object(map) => {
120 for (key, nested_value) in map {
121 let next_prefix = match prefix {
122 Some(parent) => format!("{parent}.{key}"),
123 None => key.clone(),
124 };
125 flatten_value(Some(next_prefix.as_str()), nested_value, out);
126 }
127 }
128 Value::Array(values) => {
129 for (index, nested_value) in values.iter().enumerate() {
130 let next_prefix = match prefix {
131 Some(parent) => format!("{parent}[{index}]"),
132 None => format!("[{index}]"),
133 };
134 flatten_value(Some(next_prefix.as_str()), nested_value, out);
135 }
136 }
137 _ => {
138 if let Some(key) = prefix {
139 out.insert(key.to_string(), value.clone());
140 }
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use serde_json::json;
148
149 use super::{coalesce_flat_row, flatten_row, flatten_rows};
150
151 #[test]
152 fn flattens_nested_objects_and_lists() {
153 let row = json!({
154 "uid": "oistes",
155 "person": { "mail": "o@uio.no" },
156 "members": ["a", "b"]
157 })
158 .as_object()
159 .cloned()
160 .expect("object");
161
162 let flattened = flatten_row(&row);
163 assert_eq!(
164 flattened.get("uid").and_then(|v| v.as_str()),
165 Some("oistes")
166 );
167 assert_eq!(
168 flattened.get("person.mail").and_then(|v| v.as_str()),
169 Some("o@uio.no")
170 );
171 assert_eq!(
172 flattened.get("members[0]").and_then(|v| v.as_str()),
173 Some("a")
174 );
175 }
176
177 #[test]
178 fn coalesces_flattened_row_back_to_nested_structure() {
179 let row = json!({
180 "id": 55753,
181 "txts.id": 27994,
182 "ipaddresses[0].id": 57171,
183 "ipaddresses[1].id": 57172,
184 "metadata.asset.id": 42
185 })
186 .as_object()
187 .cloned()
188 .expect("object");
189
190 let coalesced = coalesce_flat_row(&row);
191 assert_eq!(coalesced.get("id"), Some(&json!(55753)));
192 assert_eq!(coalesced.get("txts"), Some(&json!({"id": 27994})));
193 assert_eq!(
194 coalesced.get("ipaddresses"),
195 Some(&json!([{"id": 57171}, {"id": 57172}]))
196 );
197 assert_eq!(
198 coalesced.get("metadata"),
199 Some(&json!({"asset": {"id": 42}}))
200 );
201 }
202
203 #[test]
204 fn flatten_rows_maps_each_row_independently() {
205 let rows = vec![
206 json!({"user": {"name": "alice"}})
207 .as_object()
208 .cloned()
209 .expect("object"),
210 json!({"user": {"name": "bob"}})
211 .as_object()
212 .cloned()
213 .expect("object"),
214 ];
215
216 let flattened = flatten_rows(&rows);
217 assert_eq!(flattened[0].get("user.name"), Some(&json!("alice")));
218 assert_eq!(flattened[1].get("user.name"), Some(&json!("bob")));
219 }
220
221 #[test]
222 fn coalesce_skips_invalid_or_negative_index_paths() {
223 let row = json!({
224 "items[-1].id": 1,
225 "items[*].id": 2,
226 "items[0].id": 3
227 })
228 .as_object()
229 .cloned()
230 .expect("object");
231
232 let coalesced = coalesce_flat_row(&row);
233 assert_eq!(coalesced.get("items"), Some(&json!([{"id": 3}])));
234 }
235}