1use crate::value::Value;
2
3#[derive(Debug, Clone)]
5pub struct DumpOptions {
6 pub indent: usize,
8 pub sort_keys: bool,
10}
11
12impl Default for DumpOptions {
13 fn default() -> Self {
14 DumpOptions {
15 indent: 4,
16 sort_keys: false,
17 }
18 }
19}
20
21pub fn dumps(value: &Value, options: &DumpOptions) -> String {
23 let mut lines = Vec::new();
24 render_value(value, 0, options, &mut lines);
25 if lines.is_empty() {
26 String::new()
27 } else {
28 lines.join("\n") + "\n"
29 }
30}
31
32pub fn dump<W: std::io::Write>(
34 value: &Value,
35 options: &DumpOptions,
36 mut writer: W,
37) -> Result<(), std::io::Error> {
38 let s = dumps(value, options);
39 writer.write_all(s.as_bytes())
40}
41
42fn render_value(value: &Value, depth: usize, options: &DumpOptions, lines: &mut Vec<String>) {
43 match value {
44 Value::String(s) => render_string(s, depth, lines),
45 Value::List(items) => render_list(items, depth, options, lines),
46 Value::Dict(pairs) => render_dict(pairs, depth, options, lines),
47 }
48}
49
50fn render_string(s: &str, depth: usize, lines: &mut Vec<String>) {
51 let indent = " ".repeat(depth);
52 if s.is_empty() {
53 lines.push(format!("{}>", indent));
55 } else {
56 for line in s.split('\n') {
57 lines.push(format!("{}> {}", indent, line));
58 }
59 }
60}
61
62fn render_list(items: &[Value], depth: usize, options: &DumpOptions, lines: &mut Vec<String>) {
63 let indent = " ".repeat(depth);
64 if items.is_empty() {
65 lines.push(format!("{}[]", indent));
66 return;
67 }
68
69 for item in items {
70 match item {
71 Value::String(s) if s.is_empty() => {
72 lines.push(format!("{}-", indent));
73 }
74 Value::String(s) if !value_needs_multiline(s) => {
75 lines.push(format!("{}- {}", indent, s));
76 }
77 _ => {
78 lines.push(format!("{}-", indent));
79 render_value(item, depth + options.indent, options, lines);
80 }
81 }
82 }
83}
84
85fn render_dict(
86 pairs: &[(String, Value)],
87 depth: usize,
88 options: &DumpOptions,
89 lines: &mut Vec<String>,
90) {
91 let indent = " ".repeat(depth);
92 if pairs.is_empty() {
93 lines.push(format!("{}{{}}", indent));
94 return;
95 }
96
97 let pairs: Vec<&(String, Value)> = if options.sort_keys {
98 let mut sorted: Vec<&(String, Value)> = pairs.iter().collect();
99 sorted.sort_by(|a, b| a.0.cmp(&b.0));
100 sorted
101 } else {
102 pairs.iter().collect()
103 };
104
105 for (key, value) in pairs {
106 let key_needs_multiline = key_requires_multiline(key);
107
108 if key_needs_multiline {
109 for key_line in key.split('\n') {
111 if key_line.is_empty() {
112 lines.push(format!("{}:", indent));
113 } else {
114 lines.push(format!("{}: {}", indent, key_line));
115 }
116 }
117 render_value(value, depth + options.indent, options, lines);
119 } else {
120 match value {
121 Value::String(s) if s.is_empty() => {
122 lines.push(format!("{}{}:", indent, key));
123 }
124 Value::String(s) if !value_needs_multiline(s) => {
125 lines.push(format!("{}{}: {}", indent, key, s));
126 }
127 _ => {
128 lines.push(format!("{}{}:", indent, key));
130 render_value(value, depth + options.indent, options, lines);
131 }
132 }
133 }
134 }
135}
136
137fn key_requires_multiline(key: &str) -> bool {
139 if key.is_empty() || key.contains('\n') {
140 return true;
141 }
142 if key != key.trim() {
144 return true;
145 }
146 if key.contains(": ") || key.ends_with(':') {
148 return true;
149 }
150 if key.starts_with("- ")
152 || key == "-"
153 || key.starts_with("> ")
154 || key == ">"
155 || key.starts_with(": ")
156 || key == ":"
157 || key.starts_with('#')
158 || key.starts_with('[')
159 || key.starts_with('{')
160 {
161 return true;
162 }
163 false
164}
165
166fn value_needs_multiline(s: &str) -> bool {
171 if s.contains('\n') {
172 return true;
173 }
174 if !s.is_empty() && s != s.trim() {
176 return true;
177 }
178 false
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_dump_simple_string() {
187 let v = Value::String("hello".to_string());
188 assert_eq!(dumps(&v, &DumpOptions::default()), "> hello\n");
189 }
190
191 #[test]
192 fn test_dump_multiline_string() {
193 let v = Value::String("line 1\nline 2".to_string());
194 assert_eq!(dumps(&v, &DumpOptions::default()), "> line 1\n> line 2\n");
195 }
196
197 #[test]
198 fn test_dump_empty_string() {
199 let v = Value::String(String::new());
200 assert_eq!(dumps(&v, &DumpOptions::default()), ">\n");
201 }
202
203 #[test]
204 fn test_dump_simple_list() {
205 let v = Value::List(vec![
206 Value::String("a".to_string()),
207 Value::String("b".to_string()),
208 ]);
209 assert_eq!(dumps(&v, &DumpOptions::default()), "- a\n- b\n");
210 }
211
212 #[test]
213 fn test_dump_empty_list() {
214 let v = Value::List(vec![]);
215 assert_eq!(dumps(&v, &DumpOptions::default()), "[]\n");
216 }
217
218 #[test]
219 fn test_dump_simple_dict() {
220 let v = Value::Dict(vec![
221 ("name".to_string(), Value::String("John".to_string())),
222 ("age".to_string(), Value::String("30".to_string())),
223 ]);
224 assert_eq!(
225 dumps(&v, &DumpOptions::default()),
226 "name: John\nage: 30\n"
227 );
228 }
229
230 #[test]
231 fn test_dump_empty_dict() {
232 let v = Value::Dict(vec![]);
233 assert_eq!(dumps(&v, &DumpOptions::default()), "{}\n");
234 }
235
236 #[test]
237 fn test_dump_nested() {
238 let v = Value::Dict(vec![(
239 "items".to_string(),
240 Value::List(vec![
241 Value::String("a".to_string()),
242 Value::String("b".to_string()),
243 ]),
244 )]);
245 assert_eq!(
246 dumps(&v, &DumpOptions::default()),
247 "items:\n - a\n - b\n"
248 );
249 }
250
251 #[test]
252 fn test_dump_multiline_key() {
253 let v = Value::Dict(vec![(
254 "".to_string(),
255 Value::String("value".to_string()),
256 )]);
257 let result = dumps(&v, &DumpOptions::default());
258 assert_eq!(result, ":\n > value\n");
259 }
260
261 #[test]
262 fn test_dump_sorted_keys() {
263 let v = Value::Dict(vec![
264 ("b".to_string(), Value::String("2".to_string())),
265 ("a".to_string(), Value::String("1".to_string())),
266 ]);
267 let opts = DumpOptions {
268 sort_keys: true,
269 ..Default::default()
270 };
271 assert_eq!(dumps(&v, &opts), "a: 1\nb: 2\n");
272 }
273
274 #[test]
275 fn test_roundtrip_simple() {
276 use crate::parser::{loads, Top};
277 let input = "name: John\nage: 30\n";
278 let v = loads(input, Top::Any).unwrap().unwrap();
279 let output = dumps(&v, &DumpOptions::default());
280 let v2 = loads(&output, Top::Any).unwrap().unwrap();
281 assert_eq!(v, v2);
282 }
283}