1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4use toml::Table;
5
6use crate::format::{FormatOptions, FormatReader, FormatWriter};
7use crate::value::Value;
8
9fn from_toml_value(v: toml::Value) -> Value {
11 match v {
12 toml::Value::Boolean(b) => Value::Bool(b),
13 toml::Value::Integer(n) => Value::Integer(n),
14 toml::Value::Float(f) => Value::Float(f),
15 toml::Value::String(s) => Value::String(s),
16 toml::Value::Datetime(dt) => Value::String(dt.to_string()),
17 toml::Value::Array(arr) => Value::Array(arr.into_iter().map(from_toml_value).collect()),
18 toml::Value::Table(table) => {
19 let obj: IndexMap<String, Value> = table
20 .into_iter()
21 .map(|(k, v)| (k, from_toml_value(v)))
22 .collect();
23 Value::Object(obj)
24 }
25 }
26}
27
28fn to_toml_value(v: &Value) -> toml::Value {
32 match v {
33 Value::Null => toml::Value::String("null".to_string()),
34 Value::Bool(b) => toml::Value::Boolean(*b),
35 Value::Integer(n) => toml::Value::Integer(*n),
36 Value::Float(f) => {
37 if f.is_nan() || f.is_infinite() {
38 toml::Value::String(f.to_string())
39 } else {
40 toml::Value::Float(*f)
41 }
42 }
43 Value::String(s) => toml::Value::String(s.clone()),
44 Value::Array(arr) => toml::Value::Array(arr.iter().map(to_toml_value).collect()),
45 Value::Object(map) => {
46 let table: Table = map
47 .iter()
48 .map(|(k, v)| (k.clone(), to_toml_value(v)))
49 .collect();
50 toml::Value::Table(table)
51 }
52 }
53}
54
55pub struct TomlReader;
57
58fn byte_offset_to_line_col(s: &str, offset: usize) -> (usize, usize) {
60 let before = &s[..offset.min(s.len())];
61 let line = before.chars().filter(|&c| c == '\n').count() + 1;
62 let column = before.rfind('\n').map(|p| offset - p).unwrap_or(offset + 1);
63 (line, column)
64}
65
66impl FormatReader for TomlReader {
67 fn read(&self, input: &str) -> anyhow::Result<Value> {
68 let toml_val: toml::Value = toml::from_str(input).map_err(|e: toml::de::Error| {
69 if let Some(span) = e.span() {
70 let (line, column) = byte_offset_to_line_col(input, span.start);
71 let line_text = input
72 .lines()
73 .nth(line.saturating_sub(1))
74 .unwrap_or("")
75 .to_string();
76 crate::error::DkitError::ParseErrorAt {
77 format: "TOML".to_string(),
78 source: Box::new(e),
79 line,
80 column,
81 line_text,
82 }
83 } else {
84 crate::error::DkitError::ParseError {
85 format: "TOML".to_string(),
86 source: Box::new(e),
87 }
88 }
89 })?;
90 Ok(from_toml_value(toml_val))
91 }
92
93 fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
94 let mut input = String::new();
95 reader
96 .read_to_string(&mut input)
97 .map_err(|e| crate::error::DkitError::ParseError {
98 format: "TOML".to_string(),
99 source: Box::new(e),
100 })?;
101 self.read(&input)
102 }
103}
104
105#[derive(Default)]
107pub struct TomlWriter {
108 options: FormatOptions,
109}
110
111impl TomlWriter {
112 pub fn new(options: FormatOptions) -> Self {
113 Self { options }
114 }
115}
116
117impl FormatWriter for TomlWriter {
118 fn write(&self, value: &Value) -> anyhow::Result<String> {
119 let toml_val = match value {
122 Value::Object(_) => to_toml_value(value),
123 _ => {
124 let mut table = Table::new();
125 table.insert("data".to_string(), to_toml_value(value));
126 toml::Value::Table(table)
127 }
128 };
129
130 let output = if self.options.pretty {
131 toml::to_string_pretty(&toml_val)
132 } else {
133 toml::to_string(&toml_val)
134 };
135
136 output.map_err(|e| {
137 crate::error::DkitError::WriteError {
138 format: "TOML".to_string(),
139 source: Box::new(e),
140 }
141 .into()
142 })
143 }
144
145 fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
146 let output = self.write(value)?;
147 writer
148 .write_all(output.as_bytes())
149 .map_err(|e| crate::error::DkitError::WriteError {
150 format: "TOML".to_string(),
151 source: Box::new(e),
152 })?;
153 Ok(())
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
164 fn test_convert_bool() {
165 assert_eq!(
166 from_toml_value(toml::Value::Boolean(true)),
167 Value::Bool(true)
168 );
169 assert_eq!(
170 from_toml_value(toml::Value::Boolean(false)),
171 Value::Bool(false)
172 );
173 }
174
175 #[test]
176 fn test_convert_integer() {
177 assert_eq!(
178 from_toml_value(toml::Value::Integer(42)),
179 Value::Integer(42)
180 );
181 }
182
183 #[test]
184 fn test_convert_float() {
185 assert_eq!(
186 from_toml_value(toml::Value::Float(3.14)),
187 Value::Float(3.14)
188 );
189 }
190
191 #[test]
192 fn test_convert_string() {
193 assert_eq!(
194 from_toml_value(toml::Value::String("hello".to_string())),
195 Value::String("hello".to_string())
196 );
197 }
198
199 #[test]
200 fn test_convert_datetime() {
201 let dt_str = "2024-01-15T10:30:00";
202 let toml_input = format!("dt = {dt_str}");
203 let table: Table = toml::from_str(&toml_input).unwrap();
204 let val = from_toml_value(table["dt"].clone());
205 assert_eq!(val, Value::String(dt_str.to_string()));
206 }
207
208 #[test]
209 fn test_convert_array() {
210 let arr = toml::Value::Array(vec![
211 toml::Value::Integer(1),
212 toml::Value::String("two".to_string()),
213 toml::Value::Boolean(true),
214 ]);
215 let v = from_toml_value(arr);
216 let arr = v.as_array().unwrap();
217 assert_eq!(arr.len(), 3);
218 assert_eq!(arr[0], Value::Integer(1));
219 assert_eq!(arr[1], Value::String("two".to_string()));
220 assert_eq!(arr[2], Value::Bool(true));
221 }
222
223 #[test]
224 fn test_convert_table() {
225 let mut table = Table::new();
226 table.insert("name".to_string(), toml::Value::String("dkit".to_string()));
227 table.insert("version".to_string(), toml::Value::Integer(1));
228 let v = from_toml_value(toml::Value::Table(table));
229 let obj = v.as_object().unwrap();
230 assert_eq!(obj.get("name"), Some(&Value::String("dkit".to_string())));
231 assert_eq!(obj.get("version"), Some(&Value::Integer(1)));
232 }
233
234 #[test]
235 fn test_convert_nested() {
236 let toml_input = r#"
237[server]
238host = "localhost"
239port = 8080
240
241[server.tls]
242enabled = true
243"#;
244 let toml_val: toml::Value = toml::from_str(toml_input).unwrap();
245 let v = from_toml_value(toml_val);
246 let server = v.as_object().unwrap().get("server").unwrap();
247 assert_eq!(
248 server.as_object().unwrap().get("host"),
249 Some(&Value::String("localhost".to_string()))
250 );
251 assert_eq!(
252 server.as_object().unwrap().get("port"),
253 Some(&Value::Integer(8080))
254 );
255 let tls = server.as_object().unwrap().get("tls").unwrap();
256 assert_eq!(
257 tls.as_object().unwrap().get("enabled"),
258 Some(&Value::Bool(true))
259 );
260 }
261
262 #[test]
265 fn test_roundtrip_primitives() {
266 let values = vec![
267 Value::Bool(false),
268 Value::Integer(100),
269 Value::Float(2.718),
270 Value::String("test".to_string()),
271 ];
272 for v in values {
273 let toml_v = to_toml_value(&v);
274 let back = from_toml_value(toml_v);
275 assert_eq!(back, v);
276 }
277 }
278
279 #[test]
280 fn test_roundtrip_complex() {
281 let mut map = IndexMap::new();
282 map.insert(
283 "key".to_string(),
284 Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
285 );
286 let original = Value::Object(map);
287 let toml_v = to_toml_value(&original);
288 let back = from_toml_value(toml_v);
289 assert_eq!(back, original);
290 }
291
292 #[test]
293 fn test_null_converts_to_string() {
294 let toml_v = to_toml_value(&Value::Null);
295 assert_eq!(toml_v, toml::Value::String("null".to_string()));
296 }
297
298 #[test]
299 fn test_nan_converts_to_string() {
300 let toml_v = to_toml_value(&Value::Float(f64::NAN));
301 assert_eq!(toml_v, toml::Value::String("NaN".to_string()));
302 }
303
304 #[test]
305 fn test_infinity_converts_to_string() {
306 let toml_v = to_toml_value(&Value::Float(f64::INFINITY));
307 assert_eq!(toml_v, toml::Value::String("inf".to_string()));
308 }
309
310 #[test]
313 fn test_reader_simple_table() {
314 let reader = TomlReader;
315 let v = reader.read("name = \"dkit\"\ncount = 42").unwrap();
316 let obj = v.as_object().unwrap();
317 assert_eq!(obj.get("name"), Some(&Value::String("dkit".to_string())));
318 assert_eq!(obj.get("count"), Some(&Value::Integer(42)));
319 }
320
321 #[test]
322 fn test_reader_nested_table() {
323 let reader = TomlReader;
324 let toml_input = r#"
325[package]
326name = "dkit"
327version = "0.1.0"
328"#;
329 let v = reader.read(toml_input).unwrap();
330 let pkg = v.as_object().unwrap().get("package").unwrap();
331 assert_eq!(
332 pkg.as_object().unwrap().get("name"),
333 Some(&Value::String("dkit".to_string()))
334 );
335 }
336
337 #[test]
338 fn test_reader_array_of_tables() {
339 let reader = TomlReader;
340 let toml_input = r#"
341[[users]]
342name = "Alice"
343age = 30
344
345[[users]]
346name = "Bob"
347age = 25
348"#;
349 let v = reader.read(toml_input).unwrap();
350 let users = v
351 .as_object()
352 .unwrap()
353 .get("users")
354 .unwrap()
355 .as_array()
356 .unwrap();
357 assert_eq!(users.len(), 2);
358 assert_eq!(
359 users[0].as_object().unwrap().get("name"),
360 Some(&Value::String("Alice".to_string()))
361 );
362 assert_eq!(
363 users[1].as_object().unwrap().get("age"),
364 Some(&Value::Integer(25))
365 );
366 }
367
368 #[test]
369 fn test_reader_invalid_toml() {
370 let reader = TomlReader;
371 let result = reader.read("[invalid\nkey = ");
372 assert!(result.is_err());
373 }
374
375 #[test]
376 fn test_reader_empty_table() {
377 let reader = TomlReader;
378 let v = reader.read("").unwrap();
379 assert!(v.as_object().unwrap().is_empty());
380 }
381
382 #[test]
383 fn test_reader_from_reader() {
384 let reader = TomlReader;
385 let input = "x = 1".as_bytes();
386 let v = reader.read_from_reader(input).unwrap();
387 assert_eq!(v.as_object().unwrap().get("x"), Some(&Value::Integer(1)));
388 }
389
390 #[test]
391 fn test_reader_from_reader_invalid() {
392 let reader = TomlReader;
393 let input = b"[bad" as &[u8];
394 assert!(reader.read_from_reader(input).is_err());
395 }
396
397 #[test]
398 fn test_reader_inline_table() {
399 let reader = TomlReader;
400 let v = reader.read("point = { x = 1, y = 2 }").unwrap();
401 let point = v.as_object().unwrap().get("point").unwrap();
402 assert_eq!(
403 point.as_object().unwrap().get("x"),
404 Some(&Value::Integer(1))
405 );
406 assert_eq!(
407 point.as_object().unwrap().get("y"),
408 Some(&Value::Integer(2))
409 );
410 }
411
412 #[test]
413 fn test_reader_multiline_string() {
414 let reader = TomlReader;
415 let toml_input = "desc = \"\"\"\nline1\nline2\"\"\"";
416 let v = reader.read(toml_input).unwrap();
417 let desc = v
418 .as_object()
419 .unwrap()
420 .get("desc")
421 .unwrap()
422 .as_str()
423 .unwrap();
424 assert!(desc.contains("line1"));
425 assert!(desc.contains("line2"));
426 }
427
428 #[test]
429 fn test_reader_datetime() {
430 let reader = TomlReader;
431 let v = reader.read("created = 2024-01-15T10:30:00").unwrap();
432 let created = v.as_object().unwrap().get("created").unwrap();
433 assert_eq!(created, &Value::String("2024-01-15T10:30:00".to_string()));
434 }
435
436 #[test]
439 fn test_writer_default() {
440 let writer = TomlWriter::default();
441 let v = Value::Object({
442 let mut m = IndexMap::new();
443 m.insert("a".to_string(), Value::Integer(1));
444 m
445 });
446 let output = writer.write(&v).unwrap();
447 assert!(output.contains("a"));
448 assert!(output.contains('1'));
449 }
450
451 #[test]
452 fn test_writer_pretty() {
453 let writer = TomlWriter::new(FormatOptions {
454 pretty: true,
455 ..Default::default()
456 });
457 let mut inner = IndexMap::new();
458 inner.insert("host".to_string(), Value::String("localhost".to_string()));
459 let v = Value::Object({
460 let mut m = IndexMap::new();
461 m.insert("server".to_string(), Value::Object(inner));
462 m
463 });
464 let output = writer.write(&v).unwrap();
465 assert!(output.contains("[server]"));
466 }
467
468 #[test]
469 fn test_writer_compact() {
470 let writer = TomlWriter::new(FormatOptions {
471 pretty: false,
472 ..Default::default()
473 });
474 let v = Value::Object({
475 let mut m = IndexMap::new();
476 m.insert("a".to_string(), Value::Integer(1));
477 m
478 });
479 let output = writer.write(&v).unwrap();
480 assert!(output.contains("a = 1"));
481 }
482
483 #[test]
484 fn test_writer_wraps_non_table() {
485 let writer = TomlWriter::default();
486 let output = writer.write(&Value::Integer(42)).unwrap();
487 assert!(output.contains("data = 42"));
488 }
489
490 #[test]
491 fn test_writer_wraps_array() {
492 let writer = TomlWriter::default();
493 let v = Value::Array(vec![Value::Integer(1), Value::Integer(2)]);
494 let output = writer.write(&v).unwrap();
495 assert!(output.contains("data"));
496 }
497
498 #[test]
499 fn test_writer_null_as_string() {
500 let writer = TomlWriter::default();
501 let v = Value::Object({
502 let mut m = IndexMap::new();
503 m.insert("val".to_string(), Value::Null);
504 m
505 });
506 let output = writer.write(&v).unwrap();
507 assert!(output.contains("\"null\""));
508 }
509
510 #[test]
511 fn test_writer_to_writer() {
512 let writer = TomlWriter::default();
513 let v = Value::Object({
514 let mut m = IndexMap::new();
515 m.insert("x".to_string(), Value::Integer(42));
516 m
517 });
518 let mut buf = Vec::new();
519 writer.write_to_writer(&v, &mut buf).unwrap();
520 let output = String::from_utf8(buf).unwrap();
521 assert!(output.contains("x"));
522 assert!(output.contains("42"));
523 }
524
525 #[test]
528 fn test_full_roundtrip() {
529 let toml_input = r#"
530name = "dkit"
531version = 1
532
533[settings]
534debug = false
535threshold = 0.5
536
537[settings.nested]
538key = "value"
539"#;
540 let reader = TomlReader;
541 let writer = TomlWriter::new(FormatOptions {
542 pretty: true,
543 ..Default::default()
544 });
545
546 let value = reader.read(toml_input).unwrap();
547 let toml_output = writer.write(&value).unwrap();
548 let value2 = reader.read(&toml_output).unwrap();
549
550 assert_eq!(value, value2);
551 }
552
553 #[test]
554 fn test_roundtrip_array_of_tables() {
555 let toml_input = r#"
556[[items]]
557name = "a"
558count = 1
559
560[[items]]
561name = "b"
562count = 2
563"#;
564 let reader = TomlReader;
565 let writer = TomlWriter::new(FormatOptions {
566 pretty: true,
567 ..Default::default()
568 });
569
570 let value = reader.read(toml_input).unwrap();
571 let toml_output = writer.write(&value).unwrap();
572 let value2 = reader.read(&toml_output).unwrap();
573
574 assert_eq!(value, value2);
575 }
576
577 #[test]
580 fn test_unicode_string() {
581 let reader = TomlReader;
582 let v = reader.read("emoji = \"🎉\"\nkorean = \"한글\"").unwrap();
583 let obj = v.as_object().unwrap();
584 assert_eq!(obj.get("emoji"), Some(&Value::String("🎉".to_string())));
585 assert_eq!(obj.get("korean"), Some(&Value::String("한글".to_string())));
586 }
587
588 #[test]
589 fn test_negative_numbers() {
590 let reader = TomlReader;
591 let v = reader.read("neg_int = -42\nneg_float = -3.14").unwrap();
592 let obj = v.as_object().unwrap();
593 assert_eq!(obj.get("neg_int"), Some(&Value::Integer(-42)));
594 assert_eq!(obj.get("neg_float"), Some(&Value::Float(-3.14)));
595 }
596
597 #[test]
598 fn test_deeply_nested() {
599 let toml_input = r#"
600[a.b.c]
601d = 1
602"#;
603 let reader = TomlReader;
604 let v = reader.read(toml_input).unwrap();
605 let d = v
606 .as_object()
607 .unwrap()
608 .get("a")
609 .unwrap()
610 .as_object()
611 .unwrap()
612 .get("b")
613 .unwrap()
614 .as_object()
615 .unwrap()
616 .get("c")
617 .unwrap()
618 .as_object()
619 .unwrap()
620 .get("d")
621 .unwrap();
622 assert_eq!(d, &Value::Integer(1));
623 }
624
625 #[test]
626 fn test_boolean_values() {
627 let reader = TomlReader;
628 let v = reader.read("yes_val = true\nno_val = false").unwrap();
629 let obj = v.as_object().unwrap();
630 assert_eq!(obj.get("yes_val"), Some(&Value::Bool(true)));
631 assert_eq!(obj.get("no_val"), Some(&Value::Bool(false)));
632 }
633
634 #[test]
635 fn test_array_of_mixed_types_string() {
636 let reader = TomlReader;
638 let v = reader.read("tags = [\"rust\", \"cli\", \"tool\"]").unwrap();
639 let tags = v
640 .as_object()
641 .unwrap()
642 .get("tags")
643 .unwrap()
644 .as_array()
645 .unwrap();
646 assert_eq!(tags.len(), 3);
647 assert_eq!(tags[0], Value::String("rust".to_string()));
648 }
649
650 #[test]
651 fn test_large_integer() {
652 let reader = TomlReader;
653 let v = reader.read("big = 9223372036854775807").unwrap();
654 assert_eq!(
655 v.as_object().unwrap().get("big"),
656 Some(&Value::Integer(i64::MAX))
657 );
658 }
659}