Skip to main content

dkit_core/format/
toml.rs

1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4use toml::Table;
5
6use crate::format::{FormatOptions, FormatReader, FormatWriter};
7use crate::value::Value;
8
9/// toml::Value → 내부 Value 변환
10fn 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
28/// 내부 Value → toml::Value 변환
29///
30/// TOML에는 Null 타입이 없으므로 Null → String("null")로 변환한다.
31fn 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
55/// TOML 포맷 Reader
56pub struct TomlReader;
57
58/// Convert a byte offset into a 1-indexed (line, column) pair.
59fn 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/// TOML 포맷 Writer
106#[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        // TOML 최상위는 반드시 테이블이어야 한다.
120        // 배열이나 프리미티브가 오면 "data" 키로 감싼다.
121        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    // --- from_toml_value 변환 테스트 ---
162
163    #[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    // --- to_toml_value 왕복 변환 테스트 ---
263
264    #[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    // --- TomlReader 테스트 ---
311
312    #[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    // --- TomlWriter 테스트 ---
437
438    #[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    // --- 왕복 변환 테스트 (Reader → Writer → Reader) ---
526
527    #[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    // --- 특수 케이스 ---
578
579    #[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        // TOML arrays must be homogeneous; mixed types are not valid TOML
637        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}