Skip to main content

osp_cli/dsl/eval/
flatten.rs

1use 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}