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
11impl FieldTree {
12    /// Iterates the top-level field names requested at this level of the tree.
13    pub(crate) fn top_level_names(&self) -> impl Iterator<Item = &str> {
14        self.children.keys().map(String::as_str)
15    }
16}
17
18/// Parses comma-separated field paths.
19#[must_use]
20pub fn parse_fields(fields: &str) -> FieldTree {
21    let mut root = FieldTree::default();
22    for part in fields
23        .split(',')
24        .map(str::trim)
25        .filter(|part| !part.is_empty())
26    {
27        insert_path(&mut root, part);
28    }
29    root
30}
31
32/// Applies field projection to objects or arrays of objects.
33#[must_use]
34pub fn filter_fields(data: &Value, fields: &str) -> Value {
35    let fields = fields.trim();
36    if fields.is_empty() || fields == "all" || fields == "*" {
37        return data.clone();
38    }
39    project_fields(data, &parse_fields(fields))
40}
41
42/// Applies an already-parsed field-selection tree, so a caller that also
43/// needs the tree (e.g. to validate requested names) can parse the
44/// `--fields` string once and reuse it here instead of paying for
45/// [`parse_fields`] a second time.
46pub(crate) fn project_fields(data: &Value, allowed: &FieldTree) -> Value {
47    match data {
48        Value::Array(items) => {
49            if items
50                .iter()
51                .any(|item| !matches!(item, Value::Object(_) | Value::Null))
52            {
53                return data.clone();
54            }
55            Value::Array(
56                items
57                    .iter()
58                    .map(|item| match item {
59                        Value::Object(map) => Value::Object(filter_map(map, allowed)),
60                        other => other.clone(),
61                    })
62                    .collect(),
63            )
64        }
65        Value::Object(map) => Value::Object(filter_map(map, allowed)),
66        other => other.clone(),
67    }
68}
69
70fn insert_path(tree: &mut FieldTree, path: &str) {
71    let Some((top, rest)) = path.split_once('.') else {
72        tree.children.insert(path.to_owned(), None);
73        return;
74    };
75    if matches!(tree.children.get(top), Some(None)) {
76        return;
77    }
78    let subtree = tree
79        .children
80        .entry(top.to_owned())
81        .or_insert_with(|| Some(Box::<FieldTree>::default()));
82    if let Some(subtree) = subtree {
83        insert_path(subtree, rest);
84    }
85}
86
87fn filter_map(map: &Map<String, Value>, allowed: &FieldTree) -> Map<String, Value> {
88    let mut out = Map::new();
89    for (key, subtree) in &allowed.children {
90        let Some(value) = map.get(key) else {
91            continue;
92        };
93        let filtered = match subtree {
94            None => value.clone(),
95            Some(subtree) => filter_nested(value, subtree),
96        };
97        out.insert(key.clone(), filtered);
98    }
99    out
100}
101
102fn filter_nested(value: &Value, subtree: &FieldTree) -> Value {
103    match value {
104        Value::Object(map) => Value::Object(filter_map(map, subtree)),
105        Value::Array(items) => Value::Array(
106            items
107                .iter()
108                .map(|item| match item {
109                    Value::Object(map) => Value::Object(filter_map(map, subtree)),
110                    other => other.clone(),
111                })
112                .collect(),
113        ),
114        other => other.clone(),
115    }
116}