Skip to main content

dkit_core/format/
yaml.rs

1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatOptions, FormatReader, FormatWriter};
6use crate::value::Value;
7
8/// serde_yaml::Value → 내부 Value 변환
9fn from_yaml_value(v: serde_yaml::Value) -> Value {
10    match v {
11        serde_yaml::Value::Null => Value::Null,
12        serde_yaml::Value::Bool(b) => Value::Bool(b),
13        serde_yaml::Value::Number(n) => {
14            if let Some(i) = n.as_i64() {
15                Value::Integer(i)
16            } else if let Some(f) = n.as_f64() {
17                Value::Float(f)
18            } else {
19                Value::Float(n.as_f64().unwrap_or(f64::NAN))
20            }
21        }
22        serde_yaml::Value::String(s) => Value::String(s),
23        serde_yaml::Value::Sequence(seq) => {
24            Value::Array(seq.into_iter().map(from_yaml_value).collect())
25        }
26        serde_yaml::Value::Mapping(map) => {
27            let obj: IndexMap<String, Value> = map
28                .into_iter()
29                .map(|(k, v)| {
30                    let key = match k {
31                        serde_yaml::Value::String(s) => s,
32                        serde_yaml::Value::Number(n) => n.to_string(),
33                        serde_yaml::Value::Bool(b) => b.to_string(),
34                        serde_yaml::Value::Null => "null".to_string(),
35                        other => serde_yaml::to_string(&other)
36                            .unwrap_or_default()
37                            .trim()
38                            .to_string(),
39                    };
40                    (key, from_yaml_value(v))
41                })
42                .collect();
43            Value::Object(obj)
44        }
45        serde_yaml::Value::Tagged(tagged) => from_yaml_value(tagged.value),
46    }
47}
48
49/// 내부 Value → serde_yaml::Value 변환
50fn to_yaml_value(v: &Value) -> serde_yaml::Value {
51    match v {
52        Value::Null => serde_yaml::Value::Null,
53        Value::Bool(b) => serde_yaml::Value::Bool(*b),
54        Value::Integer(n) => serde_yaml::Value::Number(serde_yaml::Number::from(*n)),
55        Value::Float(f) => {
56            if f.is_nan() || f.is_infinite() {
57                serde_yaml::Value::Null
58            } else {
59                serde_yaml::Value::Number(serde_yaml::Number::from(*f))
60            }
61        }
62        Value::String(s) => serde_yaml::Value::String(s.clone()),
63        Value::Array(arr) => serde_yaml::Value::Sequence(arr.iter().map(to_yaml_value).collect()),
64        Value::Object(map) => {
65            let mapping: serde_yaml::Mapping = map
66                .iter()
67                .map(|(k, v)| (serde_yaml::Value::String(k.clone()), to_yaml_value(v)))
68                .collect();
69            serde_yaml::Value::Mapping(mapping)
70        }
71    }
72}
73
74/// YAML 포맷 Reader
75pub struct YamlReader;
76
77impl FormatReader for YamlReader {
78    fn read(&self, input: &str) -> anyhow::Result<Value> {
79        let yaml_val: serde_yaml::Value =
80            serde_yaml::from_str(input).map_err(|e: serde_yaml::Error| {
81                if let Some(loc) = e.location() {
82                    let line = loc.line();
83                    let column = loc.column();
84                    let line_text = input
85                        .lines()
86                        .nth(line.saturating_sub(1))
87                        .unwrap_or("")
88                        .to_string();
89                    crate::error::DkitError::ParseErrorAt {
90                        format: "YAML".to_string(),
91                        source: Box::new(e),
92                        line,
93                        column,
94                        line_text,
95                    }
96                } else {
97                    crate::error::DkitError::ParseError {
98                        format: "YAML".to_string(),
99                        source: Box::new(e),
100                    }
101                }
102            })?;
103        Ok(from_yaml_value(yaml_val))
104    }
105
106    fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
107        let mut input = String::new();
108        reader
109            .read_to_string(&mut input)
110            .map_err(|e| crate::error::DkitError::ParseError {
111                format: "YAML".to_string(),
112                source: Box::new(e),
113            })?;
114        self.read(&input)
115    }
116}
117
118/// YAML 포맷 Writer
119#[derive(Default)]
120pub struct YamlWriter {
121    options: FormatOptions,
122}
123
124impl YamlWriter {
125    pub fn new(options: FormatOptions) -> Self {
126        Self { options }
127    }
128}
129
130impl FormatWriter for YamlWriter {
131    fn write(&self, value: &Value) -> anyhow::Result<String> {
132        let yaml_val = to_yaml_value(value);
133
134        if self.options.flow_style {
135            // Flow style: JSON-like inline format
136            let json_val = yaml_to_json_style(&yaml_val);
137            let output = serde_json::to_string(&json_val).map_err(|e| {
138                crate::error::DkitError::WriteError {
139                    format: "YAML".to_string(),
140                    source: Box::new(e),
141                }
142            })?;
143            Ok(output)
144        } else {
145            let output = serde_yaml::to_string(&yaml_val).map_err(|e| {
146                crate::error::DkitError::WriteError {
147                    format: "YAML".to_string(),
148                    source: Box::new(e),
149                }
150            })?;
151            Ok(output)
152        }
153    }
154
155    fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
156        let output = self.write(value)?;
157        writer
158            .write_all(output.as_bytes())
159            .map_err(|e| crate::error::DkitError::WriteError {
160                format: "YAML".to_string(),
161                source: Box::new(e),
162            })?;
163        Ok(())
164    }
165}
166
167/// YAML Value를 JSON-compatible value로 변환 (flow style 출력용)
168fn yaml_to_json_style(v: &serde_yaml::Value) -> serde_json::Value {
169    match v {
170        serde_yaml::Value::Null => serde_json::Value::Null,
171        serde_yaml::Value::Bool(b) => serde_json::Value::Bool(*b),
172        serde_yaml::Value::Number(n) => {
173            if let Some(i) = n.as_i64() {
174                serde_json::Value::Number(i.into())
175            } else if let Some(f) = n.as_f64() {
176                serde_json::Number::from_f64(f)
177                    .map(serde_json::Value::Number)
178                    .unwrap_or(serde_json::Value::Null)
179            } else {
180                serde_json::Value::Null
181            }
182        }
183        serde_yaml::Value::String(s) => serde_json::Value::String(s.clone()),
184        serde_yaml::Value::Sequence(seq) => {
185            serde_json::Value::Array(seq.iter().map(yaml_to_json_style).collect())
186        }
187        serde_yaml::Value::Mapping(map) => {
188            let obj: serde_json::Map<String, serde_json::Value> = map
189                .iter()
190                .map(|(k, v)| {
191                    let key = match k {
192                        serde_yaml::Value::String(s) => s.clone(),
193                        other => serde_yaml::to_string(other)
194                            .unwrap_or_default()
195                            .trim()
196                            .to_string(),
197                    };
198                    (key, yaml_to_json_style(v))
199                })
200                .collect();
201            serde_json::Value::Object(obj)
202        }
203        serde_yaml::Value::Tagged(tagged) => yaml_to_json_style(&tagged.value),
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    // --- from_yaml_value 변환 테스트 ---
212
213    #[test]
214    fn test_convert_null() {
215        let v = from_yaml_value(serde_yaml::Value::Null);
216        assert_eq!(v, Value::Null);
217    }
218
219    #[test]
220    fn test_convert_bool() {
221        assert_eq!(
222            from_yaml_value(serde_yaml::Value::Bool(true)),
223            Value::Bool(true)
224        );
225        assert_eq!(
226            from_yaml_value(serde_yaml::Value::Bool(false)),
227            Value::Bool(false)
228        );
229    }
230
231    #[test]
232    fn test_convert_integer() {
233        let yaml_val: serde_yaml::Value = serde_yaml::from_str("42").unwrap();
234        let v = from_yaml_value(yaml_val);
235        assert_eq!(v, Value::Integer(42));
236    }
237
238    #[test]
239    fn test_convert_float() {
240        let yaml_val: serde_yaml::Value = serde_yaml::from_str("3.14").unwrap();
241        let v = from_yaml_value(yaml_val);
242        assert_eq!(v, Value::Float(3.14));
243    }
244
245    #[test]
246    fn test_convert_string() {
247        let v = from_yaml_value(serde_yaml::Value::String("hello".to_string()));
248        assert_eq!(v, Value::String("hello".to_string()));
249    }
250
251    #[test]
252    fn test_convert_sequence() {
253        let yaml_val: serde_yaml::Value = serde_yaml::from_str("[1, two, null]").unwrap();
254        let v = from_yaml_value(yaml_val);
255        let arr = v.as_array().unwrap();
256        assert_eq!(arr.len(), 3);
257        assert_eq!(arr[0], Value::Integer(1));
258        assert_eq!(arr[1], Value::String("two".to_string()));
259        assert_eq!(arr[2], Value::Null);
260    }
261
262    #[test]
263    fn test_convert_mapping() {
264        let yaml = "name: dkit\nversion: 1";
265        let yaml_val: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
266        let v = from_yaml_value(yaml_val);
267        let obj = v.as_object().unwrap();
268        assert_eq!(obj.get("name"), Some(&Value::String("dkit".to_string())));
269        assert_eq!(obj.get("version"), Some(&Value::Integer(1)));
270    }
271
272    #[test]
273    fn test_convert_nested() {
274        let yaml = r#"
275users:
276  - name: Alice
277    age: 30
278  - name: Bob
279    age: 25
280"#;
281        let yaml_val: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
282        let v = from_yaml_value(yaml_val);
283        let users = v
284            .as_object()
285            .unwrap()
286            .get("users")
287            .unwrap()
288            .as_array()
289            .unwrap();
290        assert_eq!(users.len(), 2);
291        assert_eq!(
292            users[0].as_object().unwrap().get("name"),
293            Some(&Value::String("Alice".to_string()))
294        );
295        assert_eq!(
296            users[0].as_object().unwrap().get("age"),
297            Some(&Value::Integer(30))
298        );
299    }
300
301    #[test]
302    fn test_convert_numeric_key() {
303        let yaml = "123: value";
304        let yaml_val: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
305        let v = from_yaml_value(yaml_val);
306        let obj = v.as_object().unwrap();
307        assert_eq!(obj.get("123"), Some(&Value::String("value".to_string())));
308    }
309
310    // --- to_yaml_value 왕복 변환 테스트 ---
311
312    #[test]
313    fn test_roundtrip_primitives() {
314        let values = vec![
315            Value::Null,
316            Value::Bool(false),
317            Value::Integer(100),
318            Value::Float(2.718),
319            Value::String("test".to_string()),
320        ];
321        for v in values {
322            let yaml = to_yaml_value(&v);
323            let back = from_yaml_value(yaml);
324            assert_eq!(back, v);
325        }
326    }
327
328    #[test]
329    fn test_roundtrip_complex() {
330        let mut map = IndexMap::new();
331        map.insert(
332            "key".to_string(),
333            Value::Array(vec![Value::Integer(1), Value::Null]),
334        );
335        let original = Value::Object(map);
336        let yaml = to_yaml_value(&original);
337        let back = from_yaml_value(yaml);
338        assert_eq!(back, original);
339    }
340
341    // --- YamlReader 테스트 ---
342
343    #[test]
344    fn test_reader_simple_mapping() {
345        let reader = YamlReader;
346        let v = reader.read("name: dkit\ncount: 42").unwrap();
347        let obj = v.as_object().unwrap();
348        assert_eq!(obj.get("name"), Some(&Value::String("dkit".to_string())));
349        assert_eq!(obj.get("count"), Some(&Value::Integer(42)));
350    }
351
352    #[test]
353    fn test_reader_sequence() {
354        let reader = YamlReader;
355        let v = reader.read("- 1\n- 2\n- 3").unwrap();
356        assert_eq!(v.as_array().unwrap().len(), 3);
357    }
358
359    #[test]
360    fn test_reader_invalid_yaml() {
361        let reader = YamlReader;
362        let result = reader.read(":\n  :\n    - ][");
363        assert!(result.is_err());
364    }
365
366    #[test]
367    fn test_reader_empty_mapping() {
368        let reader = YamlReader;
369        let v = reader.read("{}").unwrap();
370        assert!(v.as_object().unwrap().is_empty());
371    }
372
373    #[test]
374    fn test_reader_empty_sequence() {
375        let reader = YamlReader;
376        let v = reader.read("[]").unwrap();
377        assert!(v.as_array().unwrap().is_empty());
378    }
379
380    #[test]
381    fn test_reader_from_reader() {
382        let reader = YamlReader;
383        let input = "x: 1".as_bytes();
384        let v = reader.read_from_reader(input).unwrap();
385        assert_eq!(v.as_object().unwrap().get("x"), Some(&Value::Integer(1)));
386    }
387
388    #[test]
389    fn test_reader_multiline_string() {
390        let reader = YamlReader;
391        let yaml = "description: |\n  line1\n  line2\n";
392        let v = reader.read(yaml).unwrap();
393        let desc = v
394            .as_object()
395            .unwrap()
396            .get("description")
397            .unwrap()
398            .as_str()
399            .unwrap();
400        assert!(desc.contains("line1"));
401        assert!(desc.contains("line2"));
402    }
403
404    #[test]
405    fn test_reader_anchor_alias() {
406        let reader = YamlReader;
407        let yaml =
408            "defaults: &defaults\n  timeout: 30\nserver:\n  host: localhost\n  timeout: *defaults";
409        let v = reader.read(yaml).unwrap();
410        let server = v.as_object().unwrap().get("server").unwrap();
411        assert_eq!(
412            server.as_object().unwrap().get("host"),
413            Some(&Value::String("localhost".to_string()))
414        );
415        // Alias reference resolves to the anchored value
416        let defaults = v.as_object().unwrap().get("defaults").unwrap();
417        assert_eq!(
418            defaults.as_object().unwrap().get("timeout"),
419            Some(&Value::Integer(30))
420        );
421    }
422
423    // --- YamlWriter 테스트 ---
424
425    #[test]
426    fn test_writer_default() {
427        let writer = YamlWriter::default();
428        let v = Value::Object({
429            let mut m = IndexMap::new();
430            m.insert("a".to_string(), Value::Integer(1));
431            m
432        });
433        let output = writer.write(&v).unwrap();
434        assert!(output.contains("a:"));
435        assert!(output.contains('1'));
436    }
437
438    #[test]
439    fn test_writer_flow_style() {
440        let writer = YamlWriter::new(FormatOptions {
441            flow_style: true,
442            ..Default::default()
443        });
444        let v = Value::Object({
445            let mut m = IndexMap::new();
446            m.insert("a".to_string(), Value::Integer(1));
447            m
448        });
449        let output = writer.write(&v).unwrap();
450        // Flow style produces JSON-like inline output
451        assert!(output.contains('{'));
452        assert!(output.contains('}'));
453    }
454
455    #[test]
456    fn test_writer_null() {
457        let writer = YamlWriter::default();
458        let output = writer.write(&Value::Null).unwrap();
459        assert!(output.contains("null"));
460    }
461
462    #[test]
463    fn test_writer_to_writer() {
464        let writer = YamlWriter::default();
465        let mut buf = Vec::new();
466        writer
467            .write_to_writer(&Value::Integer(42), &mut buf)
468            .unwrap();
469        let output = String::from_utf8(buf).unwrap();
470        assert!(output.contains("42"));
471    }
472
473    #[test]
474    fn test_writer_nan_becomes_null() {
475        let writer = YamlWriter::default();
476        let output = writer.write(&Value::Float(f64::NAN)).unwrap();
477        assert!(output.contains("null"));
478    }
479
480    // --- 왕복 변환 테스트 (Reader → Writer → Reader) ---
481
482    #[test]
483    fn test_full_roundtrip() {
484        let yaml_input = "name: dkit\nversion: 1\ntags:\n- rust\n- cli\n";
485        let reader = YamlReader;
486        let writer = YamlWriter::default();
487
488        let value = reader.read(yaml_input).unwrap();
489        let yaml_output = writer.write(&value).unwrap();
490        let value2 = reader.read(&yaml_output).unwrap();
491
492        assert_eq!(value, value2);
493    }
494
495    // --- 특수 케이스 ---
496
497    #[test]
498    fn test_unicode_string() {
499        let reader = YamlReader;
500        let v = reader.read("emoji: 🎉\nkorean: 한글").unwrap();
501        let obj = v.as_object().unwrap();
502        assert_eq!(obj.get("emoji"), Some(&Value::String("🎉".to_string())));
503        assert_eq!(obj.get("korean"), Some(&Value::String("한글".to_string())));
504    }
505
506    #[test]
507    fn test_negative_numbers() {
508        let reader = YamlReader;
509        let v = reader.read("neg_int: -42\nneg_float: -3.14").unwrap();
510        let obj = v.as_object().unwrap();
511        assert_eq!(obj.get("neg_int"), Some(&Value::Integer(-42)));
512        assert_eq!(obj.get("neg_float"), Some(&Value::Float(-3.14)));
513    }
514
515    #[test]
516    fn test_deeply_nested() {
517        let yaml = "a:\n  b:\n    c:\n      d: 1";
518        let reader = YamlReader;
519        let v = reader.read(yaml).unwrap();
520        let d = v
521            .as_object()
522            .unwrap()
523            .get("a")
524            .unwrap()
525            .as_object()
526            .unwrap()
527            .get("b")
528            .unwrap()
529            .as_object()
530            .unwrap()
531            .get("c")
532            .unwrap()
533            .as_object()
534            .unwrap()
535            .get("d")
536            .unwrap();
537        assert_eq!(d, &Value::Integer(1));
538    }
539
540    #[test]
541    fn test_boolean_values() {
542        let reader = YamlReader;
543        let v = reader.read("yes_val: true\nno_val: false").unwrap();
544        let obj = v.as_object().unwrap();
545        assert_eq!(obj.get("yes_val"), Some(&Value::Bool(true)));
546        assert_eq!(obj.get("no_val"), Some(&Value::Bool(false)));
547    }
548
549    #[test]
550    fn test_null_variants() {
551        let reader = YamlReader;
552        let v = reader.read("a: null\nb: ~\nc:").unwrap();
553        let obj = v.as_object().unwrap();
554        assert_eq!(obj.get("a"), Some(&Value::Null));
555        assert_eq!(obj.get("b"), Some(&Value::Null));
556        assert_eq!(obj.get("c"), Some(&Value::Null));
557    }
558}