Skip to main content

cli_engine/output/
fields.rs

1use std::collections::BTreeMap;
2
3use serde_json::{Map, Value};
4
5/// Parsed field-selection tree.
6#[derive(Clone, Debug, Default, Eq, PartialEq)]
7pub struct FieldTree {
8    children: BTreeMap<String, Option<Box<FieldTree>>>,
9}
10
11/// Parses comma-separated field paths.
12#[must_use]
13pub fn parse_fields(fields: &str) -> FieldTree {
14    let mut root = FieldTree::default();
15    for part in fields
16        .split(',')
17        .map(str::trim)
18        .filter(|part| !part.is_empty())
19    {
20        insert_path(&mut root, part);
21    }
22    root
23}
24
25/// Applies field projection to objects or arrays of objects.
26#[must_use]
27pub fn filter_fields(data: &Value, fields: &str) -> Value {
28    let fields = fields.trim();
29    if fields.is_empty() || fields == "all" || fields == "*" {
30        return data.clone();
31    }
32    let allowed = parse_fields(fields);
33    match data {
34        Value::Array(items) => {
35            if items
36                .iter()
37                .any(|item| !matches!(item, Value::Object(_) | Value::Null))
38            {
39                return data.clone();
40            }
41            Value::Array(
42                items
43                    .iter()
44                    .map(|item| match item {
45                        Value::Object(map) => Value::Object(filter_map(map, &allowed)),
46                        other => other.clone(),
47                    })
48                    .collect(),
49            )
50        }
51        Value::Object(map) => Value::Object(filter_map(map, &allowed)),
52        other => other.clone(),
53    }
54}
55
56fn insert_path(tree: &mut FieldTree, path: &str) {
57    let Some((top, rest)) = path.split_once('.') else {
58        tree.children.insert(path.to_owned(), None);
59        return;
60    };
61    if matches!(tree.children.get(top), Some(None)) {
62        return;
63    }
64    let subtree = tree
65        .children
66        .entry(top.to_owned())
67        .or_insert_with(|| Some(Box::<FieldTree>::default()));
68    if let Some(subtree) = subtree {
69        insert_path(subtree, rest);
70    }
71}
72
73fn filter_map(map: &Map<String, Value>, allowed: &FieldTree) -> Map<String, Value> {
74    let mut out = Map::new();
75    for (key, subtree) in &allowed.children {
76        let Some(value) = map.get(key) else {
77            continue;
78        };
79        let filtered = match subtree {
80            None => value.clone(),
81            Some(subtree) => filter_nested(value, subtree),
82        };
83        out.insert(key.clone(), filtered);
84    }
85    out
86}
87
88fn filter_nested(value: &Value, subtree: &FieldTree) -> Value {
89    match value {
90        Value::Object(map) => Value::Object(filter_map(map, subtree)),
91        Value::Array(items) => Value::Array(
92            items
93                .iter()
94                .map(|item| match item {
95                    Value::Object(map) => Value::Object(filter_map(map, subtree)),
96                    other => other.clone(),
97                })
98                .collect(),
99        ),
100        other => other.clone(),
101    }
102}