1use std::io::Write;
2
3use crate::error::Result;
4
5pub enum Value {
9 Null,
10 Bool(bool),
11 Integer(i64),
12 Float(f64),
13 String(String),
14 Array(Vec<Value>),
15 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
39pub 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 let mut i = 0;
67 while i < entries.len() {
68 if entries[i].1.is_primitive() {
69 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 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 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 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
138fn 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
155fn try_as_table(items: &[Value]) -> Option<TableData> {
157 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 if !objects
172 .iter()
173 .all(|entries| entries.iter().all(|(_, v)| v.is_primitive()))
174 {
175 return None;
176 }
177
178 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 write!(writer, "|")?;
214 for h in headers {
215 write!(writer, " {} |", escape_pipe(h))?;
216 }
217 writeln!(writer)?;
218
219 write!(writer, "|")?;
221 for _ in headers {
222 write!(writer, "---|")?;
223 }
224 writeln!(writer)?;
225
226 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#[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}