Skip to main content

mq_conv/formats/
structured.rs

1use std::io::Write;
2
3use crate::error::Result;
4
5/// A format-agnostic value representation for structured data.
6/// Each format converter converts its native value type into this enum,
7/// then uses `write_value_as_markdown` to produce structured markdown output.
8pub enum Value {
9    Null,
10    Bool(bool),
11    Integer(i64),
12    Float(f64),
13    String(String),
14    Array(Vec<Value>),
15    /// Key-value pairs preserving insertion order.
16    Object(Vec<(String, Value)>),
17}
18
19impl Value {
20    fn is_primitive(&self) -> bool {
21        matches!(
22            self,
23            Value::Null | Value::Bool(_) | Value::Integer(_) | Value::Float(_) | Value::String(_)
24        )
25    }
26
27    fn display_primitive(&self) -> String {
28        match self {
29            Value::Null => String::new(),
30            Value::Bool(b) => b.to_string(),
31            Value::Integer(n) => n.to_string(),
32            Value::Float(f) => f.to_string(),
33            Value::String(s) => s.clone(),
34            Value::Array(_) | Value::Object(_) => String::new(),
35        }
36    }
37}
38
39/// Write a structured value as markdown to the given writer.
40pub fn write_value_as_markdown(writer: &mut dyn Write, value: &Value) -> Result<()> {
41    write_value(writer, value, 1)?;
42    Ok(())
43}
44
45fn write_value(writer: &mut dyn Write, value: &Value, depth: usize) -> Result<()> {
46    match value {
47        Value::Null => {
48            writeln!(writer)?;
49        }
50        Value::Bool(_) | Value::Integer(_) | Value::Float(_) | Value::String(_) => {
51            writeln!(writer, "{}", value.display_primitive())?;
52        }
53        Value::Array(items) => {
54            write_array(writer, items, depth)?;
55        }
56        Value::Object(entries) => {
57            write_object(writer, entries, depth)?;
58        }
59    }
60    Ok(())
61}
62
63fn write_object(writer: &mut dyn Write, entries: &[(String, Value)], depth: usize) -> Result<()> {
64    // Separate entries into primitive key-value pairs and complex (nested) entries.
65    // Group consecutive primitives into a table.
66    let mut i = 0;
67    while i < entries.len() {
68        if entries[i].1.is_primitive() {
69            // Collect consecutive primitive entries
70            let start = i;
71            while i < entries.len() && entries[i].1.is_primitive() {
72                i += 1;
73            }
74            let primitives = &entries[start..i];
75            write_kv_table(writer, primitives)?;
76            writeln!(writer)?;
77        } else {
78            let (key, val) = &entries[i];
79            write_heading(writer, key, depth)?;
80            write_value(writer, val, depth + 1)?;
81            i += 1;
82        }
83    }
84    Ok(())
85}
86
87fn write_array(writer: &mut dyn Write, items: &[Value], depth: usize) -> Result<()> {
88    if items.is_empty() {
89        writeln!(writer, "*empty*")?;
90        return Ok(());
91    }
92
93    // Check if all items are objects with similar keys → render as table
94    if let Some(table) = try_as_table(items) {
95        write_markdown_table(writer, &table.headers, &table.rows)?;
96        writeln!(writer)?;
97        return Ok(());
98    }
99
100    // Check if all items are primitives → render as bullet list
101    if items.iter().all(|v| v.is_primitive()) {
102        for item in items {
103            writeln!(writer, "- {}", item.display_primitive())?;
104        }
105        writeln!(writer)?;
106        return Ok(());
107    }
108
109    // Mixed array: render each item
110    for (idx, item) in items.iter().enumerate() {
111        match item {
112            v if v.is_primitive() => {
113                writeln!(writer, "- {}", v.display_primitive())?;
114            }
115            Value::Object(entries) => {
116                write_heading(writer, &format!("{}", idx + 1), depth)?;
117                write_object(writer, entries, depth + 1)?;
118            }
119            Value::Array(inner) => {
120                write_heading(writer, &format!("{}", idx + 1), depth)?;
121                write_array(writer, inner, depth + 1)?;
122            }
123            _ => {}
124        }
125    }
126
127    Ok(())
128}
129
130fn write_heading(writer: &mut dyn Write, text: &str, depth: usize) -> Result<()> {
131    let level = depth.min(6);
132    let hashes = "#".repeat(level);
133    writeln!(writer, "{hashes} {text}")?;
134    writeln!(writer)?;
135    Ok(())
136}
137
138/// Write a set of primitive key-value pairs as a markdown table.
139fn write_kv_table(writer: &mut dyn Write, entries: &[(String, Value)]) -> Result<()> {
140    writeln!(writer, "| Key | Value |")?;
141    writeln!(writer, "|---|---|")?;
142    for (key, val) in entries {
143        let escaped_key = escape_pipe(key);
144        let escaped_val = escape_pipe(&val.display_primitive());
145        writeln!(writer, "| {escaped_key} | {escaped_val} |")?;
146    }
147    Ok(())
148}
149
150struct TableData {
151    headers: Vec<String>,
152    rows: Vec<Vec<String>>,
153}
154
155/// Try to interpret an array of values as a table (array of objects with common keys).
156fn try_as_table(items: &[Value]) -> Option<TableData> {
157    // All items must be objects
158    let objects: Vec<&Vec<(String, Value)>> = items
159        .iter()
160        .filter_map(|v| match v {
161            Value::Object(entries) => Some(entries),
162            _ => None,
163        })
164        .collect();
165
166    if objects.len() != items.len() || objects.is_empty() {
167        return None;
168    }
169
170    // All values in the objects must be primitives
171    if !objects
172        .iter()
173        .all(|entries| entries.iter().all(|(_, v)| v.is_primitive()))
174    {
175        return None;
176    }
177
178    // Collect all unique keys preserving order from first object
179    let mut headers: Vec<String> = Vec::new();
180    for entries in &objects {
181        for (key, _) in *entries {
182            if !headers.contains(key) {
183                headers.push(key.clone());
184            }
185        }
186    }
187
188    let rows: Vec<Vec<String>> = objects
189        .iter()
190        .map(|entries| {
191            headers
192                .iter()
193                .map(|h| {
194                    entries
195                        .iter()
196                        .find(|(k, _)| k == h)
197                        .map(|(_, v)| v.display_primitive())
198                        .unwrap_or_default()
199                })
200                .collect()
201        })
202        .collect();
203
204    Some(TableData { headers, rows })
205}
206
207fn write_markdown_table(
208    writer: &mut dyn Write,
209    headers: &[String],
210    rows: &[Vec<String>],
211) -> Result<()> {
212    // Header row
213    write!(writer, "|")?;
214    for h in headers {
215        write!(writer, " {} |", escape_pipe(h))?;
216    }
217    writeln!(writer)?;
218
219    // Separator row
220    write!(writer, "|")?;
221    for _ in headers {
222        write!(writer, "---|")?;
223    }
224    writeln!(writer)?;
225
226    // Data rows
227    for row in rows {
228        write!(writer, "|")?;
229        for (i, cell) in row.iter().enumerate() {
230            if i < headers.len() {
231                write!(writer, " {} |", escape_pipe(cell))?;
232            }
233        }
234        writeln!(writer)?;
235    }
236
237    Ok(())
238}
239
240fn escape_pipe(s: &str) -> String {
241    s.replace('|', "\\|")
242}
243
244// --- Conversions from format-specific value types ---
245
246#[cfg(feature = "json")]
247impl From<serde_json::Value> for Value {
248    fn from(v: serde_json::Value) -> Self {
249        match v {
250            serde_json::Value::Null => Value::Null,
251            serde_json::Value::Bool(b) => Value::Bool(b),
252            serde_json::Value::Number(n) => {
253                if let Some(i) = n.as_i64() {
254                    Value::Integer(i)
255                } else {
256                    Value::Float(n.as_f64().unwrap_or(0.0))
257                }
258            }
259            serde_json::Value::String(s) => Value::String(s),
260            serde_json::Value::Array(arr) => {
261                Value::Array(arr.into_iter().map(Value::from).collect())
262            }
263            serde_json::Value::Object(map) => {
264                Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
265            }
266        }
267    }
268}
269
270#[cfg(feature = "toml_conv")]
271impl From<toml::Value> for Value {
272    fn from(v: toml::Value) -> Self {
273        match v {
274            toml::Value::String(s) => Value::String(s),
275            toml::Value::Integer(i) => Value::Integer(i),
276            toml::Value::Float(f) => Value::Float(f),
277            toml::Value::Boolean(b) => Value::Bool(b),
278            toml::Value::Datetime(dt) => Value::String(dt.to_string()),
279            toml::Value::Array(arr) => Value::Array(arr.into_iter().map(Value::from).collect()),
280            toml::Value::Table(map) => {
281                Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
282            }
283        }
284    }
285}
286
287#[cfg(feature = "yaml")]
288impl From<serde_yaml::Value> for Value {
289    fn from(v: serde_yaml::Value) -> Self {
290        match v {
291            serde_yaml::Value::Null => Value::Null,
292            serde_yaml::Value::Bool(b) => Value::Bool(b),
293            serde_yaml::Value::Number(n) => {
294                if let Some(i) = n.as_i64() {
295                    Value::Integer(i)
296                } else {
297                    Value::Float(n.as_f64().unwrap_or(0.0))
298                }
299            }
300            serde_yaml::Value::String(s) => Value::String(s),
301            serde_yaml::Value::Sequence(arr) => {
302                Value::Array(arr.into_iter().map(Value::from).collect())
303            }
304            serde_yaml::Value::Mapping(map) => Value::Object(
305                map.into_iter()
306                    .map(|(k, v)| {
307                        let key = match k {
308                            serde_yaml::Value::String(s) => s,
309                            serde_yaml::Value::Number(n) => n.to_string(),
310                            serde_yaml::Value::Bool(b) => b.to_string(),
311                            _ => format!("{k:?}"),
312                        };
313                        (key, Value::from(v))
314                    })
315                    .collect(),
316            ),
317            serde_yaml::Value::Tagged(tagged) => Value::from(tagged.value),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use std::f64;
325
326    use super::*;
327    use pretty_assertions::assert_eq;
328    use rstest::rstest;
329
330    fn render(value: Value) -> String {
331        let mut output = Vec::new();
332        write_value_as_markdown(&mut output, &value).unwrap();
333        String::from_utf8(output).unwrap()
334    }
335
336    #[rstest]
337    #[case::null_value(Value::Null, "\n")]
338    #[case::bool_true(Value::Bool(true), "true\n")]
339    #[case::bool_false(Value::Bool(false), "false\n")]
340    #[case::integer(Value::Integer(42), "42\n")]
341    #[case::float(Value::Float(f64::consts::PI), "3.141592653589793\n")]
342    #[case::string(Value::String("hello".into()), "hello\n")]
343    fn test_primitive_values(#[case] value: Value, #[case] expected: &str) {
344        assert_eq!(render(value), expected);
345    }
346
347    #[rstest]
348    #[case::empty_array(
349        Value::Array(vec![]),
350        "*empty*\n"
351    )]
352    #[case::primitive_array(
353        Value::Array(vec![
354            Value::String("a".into()),
355            Value::String("b".into()),
356        ]),
357        "- a\n- b\n\n"
358    )]
359    fn test_array_values(#[case] value: Value, #[case] expected: &str) {
360        assert_eq!(render(value), expected);
361    }
362
363    #[rstest]
364    fn test_object_with_primitives() {
365        let value = Value::Object(vec![
366            ("name".into(), Value::String("Alice".into())),
367            ("age".into(), Value::Integer(30)),
368        ]);
369        let expected = "\
370| Key | Value |
371|---|---|
372| name | Alice |
373| age | 30 |
374
375";
376        assert_eq!(render(value), expected);
377    }
378
379    #[rstest]
380    fn test_object_with_nested_object() {
381        let value = Value::Object(vec![
382            ("name".into(), Value::String("Alice".into())),
383            (
384                "address".into(),
385                Value::Object(vec![("city".into(), Value::String("Tokyo".into()))]),
386            ),
387        ]);
388        let output = render(value);
389        assert!(output.contains("| name | Alice |"));
390        assert!(output.contains("# address"));
391        assert!(output.contains("| city | Tokyo |"));
392    }
393
394    #[rstest]
395    fn test_array_of_objects_as_table() {
396        let value = Value::Array(vec![
397            Value::Object(vec![
398                ("id".into(), Value::Integer(1)),
399                ("name".into(), Value::String("x".into())),
400            ]),
401            Value::Object(vec![
402                ("id".into(), Value::Integer(2)),
403                ("name".into(), Value::String("y".into())),
404            ]),
405        ]);
406        let expected = "\
407| id | name |
408|---|---|
409| 1 | x |
410| 2 | y |
411
412";
413        assert_eq!(render(value), expected);
414    }
415
416    #[rstest]
417    fn test_array_of_objects_with_nested_not_table() {
418        let value = Value::Array(vec![Value::Object(vec![
419            ("id".into(), Value::Integer(1)),
420            ("tags".into(), Value::Array(vec![Value::String("a".into())])),
421        ])]);
422        let output = render(value);
423        assert!(!output.starts_with("| id |"));
424    }
425
426    #[rstest]
427    fn test_consecutive_primitives_grouped() {
428        let value = Value::Object(vec![
429            ("a".into(), Value::String("1".into())),
430            ("b".into(), Value::String("2".into())),
431            (
432                "nested".into(),
433                Value::Object(vec![("x".into(), Value::String("y".into()))]),
434            ),
435            ("c".into(), Value::String("3".into())),
436        ]);
437        let output = render(value);
438        assert!(output.contains("| a | 1 |"));
439        assert!(output.contains("| b | 2 |"));
440        assert!(output.contains("# nested"));
441        assert!(output.contains("| c | 3 |"));
442    }
443
444    #[rstest]
445    fn test_pipe_escape_in_keys_and_values() {
446        let value = Value::Object(vec![("a|b".into(), Value::String("c|d".into()))]);
447        let output = render(value);
448        assert!(output.contains("a\\|b"));
449        assert!(output.contains("c\\|d"));
450    }
451
452    #[rstest]
453    fn test_heading_depth_caps_at_6() {
454        let mut v = Value::Object(vec![("g".into(), Value::String("leaf".into()))]);
455        for key in ["f", "e", "d", "c", "b", "a"] {
456            v = Value::Object(vec![(key.into(), v)]);
457        }
458        let output = render(v);
459        assert!(output.contains("###### f") || output.contains("###### g"));
460        assert!(!output.contains("#######"));
461    }
462
463    #[rstest]
464    fn test_mixed_array_rendering() {
465        let value = Value::Array(vec![
466            Value::Integer(1),
467            Value::Object(vec![("key".into(), Value::String("val".into()))]),
468        ]);
469        let output = render(value);
470        assert!(output.contains("- 1"));
471        assert!(output.contains("# 2"));
472        assert!(output.contains("| key | val |"));
473    }
474}