Skip to main content

edikt_core/
convert.rs

1//! Data-model helpers for format conversion.
2//!
3//! Conversion projects a source document to [`Value`] and re-emits it in another
4//! format. That drops trivia by nature (it goes through the data model, not the
5//! CST), and formats with less structure than the source must flatten. These
6//! helpers are the shared machinery; the per-format emitters live in the format
7//! crates.
8
9use crate::{Feature, Value};
10
11/// Render a value as pretty (2-space) JSON with a trailing newline.
12pub fn to_pretty_json(value: &Value) -> String {
13    let mut out = String::new();
14    write_pretty(value, 0, &mut out);
15    out.push('\n');
16    out
17}
18
19fn write_pretty(value: &Value, indent: usize, out: &mut String) {
20    match value {
21        Value::Object(m) if m.is_empty() => out.push_str("{}"),
22        Value::Array(a) if a.is_empty() => out.push_str("[]"),
23        Value::Object(m) => {
24            out.push_str("{\n");
25            for (i, (k, v)) in m.iter().enumerate() {
26                indent_by(indent + 1, out);
27                out.push_str(&Value::Str(k.clone()).to_json());
28                out.push_str(": ");
29                write_pretty(v, indent + 1, out);
30                if i + 1 < m.len() {
31                    out.push(',');
32                }
33                out.push('\n');
34            }
35            indent_by(indent, out);
36            out.push('}');
37        }
38        Value::Array(a) => {
39            out.push_str("[\n");
40            for (i, v) in a.iter().enumerate() {
41                indent_by(indent + 1, out);
42                write_pretty(v, indent + 1, out);
43                if i + 1 < a.len() {
44                    out.push(',');
45                }
46                out.push('\n');
47            }
48            indent_by(indent, out);
49            out.push(']');
50        }
51        scalar => out.push_str(&scalar.to_json()),
52    }
53}
54
55fn indent_by(level: usize, out: &mut String) {
56    for _ in 0..level {
57        out.push_str("  ");
58    }
59}
60
61/// Flatten a value to `(dotted.key, string)` pairs (`{a:{b:1}}` -> `a.b = "1"`;
62/// arrays index: `{a:[1,2]}` -> `a.0`, `a.1`). Used by the flat emitters.
63pub fn flatten(value: &Value) -> Vec<(String, String)> {
64    let mut out = Vec::new();
65    walk("", value, &mut out);
66    out
67}
68
69fn walk(prefix: &str, value: &Value, out: &mut Vec<(String, String)>) {
70    match value {
71        Value::Object(m) => {
72            for (k, v) in m {
73                walk(&join_key(prefix, k), v, out);
74            }
75        }
76        Value::Array(a) => {
77            for (i, v) in a.iter().enumerate() {
78                walk(&join_key(prefix, &i.to_string()), v, out);
79            }
80        }
81        scalar => out.push((prefix.to_string(), scalar_string(scalar))),
82    }
83}
84
85fn join_key(prefix: &str, key: &str) -> String {
86    if prefix.is_empty() {
87        key.to_string()
88    } else {
89        format!("{prefix}.{key}")
90    }
91}
92
93/// The string form of a scalar (`Str` without quotes; numbers/bools/null as text).
94pub fn scalar_string(value: &Value) -> String {
95    value.to_raw_string()
96}
97
98/// Does `value` contain nesting (an object/array inside an object/array)?
99pub fn has_nesting(value: &Value) -> bool {
100    match value {
101        Value::Object(m) => m.iter().any(|(_, v)| is_container(v)),
102        Value::Array(a) => a.iter().any(is_container),
103        _ => false,
104    }
105}
106
107/// Does `value` contain an array anywhere?
108pub fn has_array(value: &Value) -> bool {
109    match value {
110        Value::Array(_) => true,
111        Value::Object(m) => m.iter().any(|(_, v)| has_array(v)),
112        _ => false,
113    }
114}
115
116fn is_container(value: &Value) -> bool {
117    matches!(value, Value::Object(_) | Value::Array(_))
118}
119
120/// The [`Feature`]s a value needs a format to have to represent it faithfully:
121/// `Arrays` if it contains a sequence, `Nesting` if it nests containers,
122/// `TypedScalars` if it holds a non-string scalar. Used to name candidate output
123/// formats when the chosen one can't hold a query result.
124pub fn features_used(value: &Value) -> Vec<Feature> {
125    let mut used = Vec::new();
126    if has_array(value) {
127        used.push(Feature::Arrays);
128    }
129    if has_nesting(value) {
130        used.push(Feature::Nesting);
131    }
132    if has_typed_scalar(value) {
133        used.push(Feature::TypedScalars);
134    }
135    used
136}
137
138/// Does `value` contain a non-string scalar (number/bool/null) anywhere?
139fn has_typed_scalar(value: &Value) -> bool {
140    match value {
141        Value::Int(_) | Value::Float(_) | Value::Bool(_) | Value::Null => true,
142        Value::Str(_) => false,
143        Value::Array(a) => a.iter().any(has_typed_scalar),
144        Value::Object(m) => m.iter().any(|(_, v)| has_typed_scalar(v)),
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn flatten_to_dotted_keys() {
154        let v = Value::Object(vec![
155            ("a".into(), Value::Object(vec![("b".into(), Value::Int(1))])),
156            (
157                "c".into(),
158                Value::Array(vec![Value::Str("x".into()), Value::Str("y".into())]),
159            ),
160        ]);
161        assert_eq!(
162            flatten(&v),
163            vec![
164                ("a.b".to_string(), "1".to_string()),
165                ("c.0".to_string(), "x".to_string()),
166                ("c.1".to_string(), "y".to_string()),
167            ]
168        );
169    }
170
171    #[test]
172    fn pretty_json_indents() {
173        let v = Value::Object(vec![(
174            "a".into(),
175            Value::Array(vec![Value::Int(1), Value::Int(2)]),
176        )]);
177        assert_eq!(to_pretty_json(&v), "{\n  \"a\": [\n    1,\n    2\n  ]\n}\n");
178        assert_eq!(to_pretty_json(&Value::Object(vec![])), "{}\n");
179    }
180
181    #[test]
182    fn feature_detection() {
183        let nested = Value::Object(vec![("a".into(), Value::Object(vec![]))]);
184        assert!(has_nesting(&nested));
185        assert!(!has_nesting(&Value::Object(vec![(
186            "a".into(),
187            Value::Int(1)
188        )])));
189        assert!(has_array(&Value::Object(vec![(
190            "a".into(),
191            Value::Array(vec![])
192        )])));
193    }
194}