Skip to main content

dkit_core/format/
ini.rs

1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatReader, FormatWriter};
6use crate::value::Value;
7
8/// INI/CFG 포맷 Reader
9///
10/// `[section]` 헤더와 `key = value` 형식의 설정 파일을 파싱하여
11/// 2-depth Object(`{ section: { key: value } }`)로 변환한다.
12///
13/// - `#` 또는 `;` 주석 지원
14/// - 빈 줄 무시
15/// - 구분자: `=` 또는 `:`
16/// - 섹션 없는 키는 최상위 오브젝트에 배치
17/// - `.cfg` 확장자도 동일 포맷으로 처리
18pub struct IniReader;
19
20impl IniReader {
21    /// 값 문자열을 적절한 Value 타입으로 변환한다.
22    fn parse_value(raw: &str) -> Value {
23        let trimmed = raw.trim();
24
25        // 빈 값
26        if trimmed.is_empty() {
27            return Value::String(String::new());
28        }
29
30        // 따옴표로 감싸진 문자열
31        if trimmed.len() >= 2
32            && ((trimmed.starts_with('"') && trimmed.ends_with('"'))
33                || (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
34        {
35            return Value::String(trimmed[1..trimmed.len() - 1].to_string());
36        }
37
38        // Boolean
39        match trimmed.to_lowercase().as_str() {
40            "true" | "yes" | "on" => return Value::Bool(true),
41            "false" | "no" | "off" => return Value::Bool(false),
42            _ => {}
43        }
44
45        // Integer
46        if let Ok(n) = trimmed.parse::<i64>() {
47            return Value::Integer(n);
48        }
49
50        // Float
51        if let Ok(f) = trimmed.parse::<f64>() {
52            return Value::Float(f);
53        }
54
55        Value::String(trimmed.to_string())
56    }
57
58    /// 인라인 주석을 제거한다.
59    /// 따옴표 내부의 `;`이나 `#`은 주석으로 처리하지 않는다.
60    fn strip_inline_comment(value_str: &str) -> &str {
61        let mut in_single_quote = false;
62        let mut in_double_quote = false;
63
64        for (i, c) in value_str.char_indices() {
65            match c {
66                '\'' if !in_double_quote => in_single_quote = !in_single_quote,
67                '"' if !in_single_quote => in_double_quote = !in_double_quote,
68                ';' | '#' if !in_single_quote && !in_double_quote => {
69                    // 주석 앞에 공백이 있는 경우만 인라인 주석으로 처리
70                    if i > 0 && value_str.as_bytes()[i - 1] == b' ' {
71                        return &value_str[..i - 1];
72                    }
73                    // 값의 시작이 ; 또는 #이면 그 자체가 주석이 아닌 빈 값 + 주석
74                    if i == 0 {
75                        return "";
76                    }
77                }
78                _ => {}
79            }
80        }
81
82        value_str
83    }
84}
85
86impl FormatReader for IniReader {
87    fn read(&self, input: &str) -> anyhow::Result<Value> {
88        let mut root = IndexMap::new();
89        let mut current_section: Option<String> = None;
90
91        for (line_num, line) in input.lines().enumerate() {
92            let trimmed = line.trim();
93
94            // 빈 줄 무시
95            if trimmed.is_empty() {
96                continue;
97            }
98
99            // 주석 무시
100            if trimmed.starts_with('#') || trimmed.starts_with(';') {
101                continue;
102            }
103
104            // 섹션 헤더: [section]
105            if trimmed.starts_with('[') {
106                if let Some(end) = trimmed.find(']') {
107                    let section_name = trimmed[1..end].trim().to_string();
108                    if section_name.is_empty() {
109                        return Err(crate::error::DkitError::ParseErrorAt {
110                            format: "INI".to_string(),
111                            source: "empty section name".to_string().into(),
112                            line: line_num + 1,
113                            column: 1,
114                            line_text: line.to_string(),
115                        }
116                        .into());
117                    }
118                    current_section = Some(section_name.clone());
119                    // 섹션이 아직 없으면 빈 Object로 초기화
120                    root.entry(section_name)
121                        .or_insert_with(|| Value::Object(IndexMap::new()));
122                    continue;
123                } else {
124                    return Err(crate::error::DkitError::ParseErrorAt {
125                        format: "INI".to_string(),
126                        source: "unclosed section header (missing ']')".to_string().into(),
127                        line: line_num + 1,
128                        column: 1,
129                        line_text: line.to_string(),
130                    }
131                    .into());
132                }
133            }
134
135            // key=value 또는 key:value 파싱
136            // 첫 번째 '=' 또는 ':' 를 구분자로 사용
137            let sep_pos = trimmed
138                .find('=')
139                .or_else(|| trimmed.find(':'))
140                .ok_or_else(|| crate::error::DkitError::ParseErrorAt {
141                    format: "INI".to_string(),
142                    source: "expected key=value or key:value format".to_string().into(),
143                    line: line_num + 1,
144                    column: 1,
145                    line_text: line.to_string(),
146                })?;
147
148            let key = trimmed[..sep_pos].trim().to_string();
149            if key.is_empty() {
150                return Err(crate::error::DkitError::ParseErrorAt {
151                    format: "INI".to_string(),
152                    source: "empty key".to_string().into(),
153                    line: line_num + 1,
154                    column: 1,
155                    line_text: line.to_string(),
156                }
157                .into());
158            }
159
160            let raw_value = &trimmed[sep_pos + 1..];
161            let value_str = Self::strip_inline_comment(raw_value);
162            let value = Self::parse_value(value_str);
163
164            match &current_section {
165                Some(section) => {
166                    // 섹션 내부의 키-값
167                    if let Some(Value::Object(section_map)) = root.get_mut(section) {
168                        section_map.insert(key, value);
169                    }
170                }
171                None => {
172                    // 섹션 없는 최상위 키-값
173                    root.insert(key, value);
174                }
175            }
176        }
177
178        Ok(Value::Object(root))
179    }
180
181    fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
182        let mut input = String::new();
183        reader
184            .read_to_string(&mut input)
185            .map_err(|e| crate::error::DkitError::ParseError {
186                format: "INI".to_string(),
187                source: Box::new(e),
188            })?;
189        self.read(&input)
190    }
191}
192
193/// INI/CFG 포맷 Writer
194///
195/// 2-depth Object를 `[section]` + `key = value` 형식으로 출력한다.
196/// 최상위 프리미티브 키는 섹션 없이 파일 상단에 출력한다.
197pub struct IniWriter;
198
199impl IniWriter {
200    /// Value를 INI 값 문자열로 변환한다.
201    fn format_value(value: &Value) -> String {
202        match value {
203            Value::String(s) => {
204                // 특수 문자가 포함되면 따옴표로 감싼다
205                if s.is_empty()
206                    || s.contains(';')
207                    || s.contains('#')
208                    || s.contains('=')
209                    || s.contains(':')
210                    || s.starts_with(' ')
211                    || s.ends_with(' ')
212                    || s.starts_with('"')
213                    || s.starts_with('\'')
214                {
215                    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
216                } else {
217                    s.clone()
218                }
219            }
220            Value::Null => String::new(),
221            Value::Bool(b) => b.to_string(),
222            Value::Integer(n) => n.to_string(),
223            Value::Float(f) => f.to_string(),
224            Value::Array(_) | Value::Object(_) => {
225                // 중첩 구조는 JSON 문자열로 직렬화
226                serde_json::to_string(value).unwrap_or_default()
227            }
228        }
229    }
230}
231
232impl FormatWriter for IniWriter {
233    fn write(&self, value: &Value) -> anyhow::Result<String> {
234        match value {
235            Value::Object(map) => {
236                let mut output = String::new();
237
238                // 1단계: 최상위 프리미티브 키 출력 (섹션 없는 키)
239                for (key, val) in map {
240                    if !matches!(val, Value::Object(_)) {
241                        output.push_str(&format!("{} = {}\n", key, Self::format_value(val)));
242                    }
243                }
244
245                // 최상위 키와 섹션 사이에 빈 줄 추가
246                let has_top_level = map.values().any(|v| !matches!(v, Value::Object(_)));
247                let has_sections = map.values().any(|v| matches!(v, Value::Object(_)));
248                if has_top_level && has_sections {
249                    output.push('\n');
250                }
251
252                // 2단계: 섹션별 출력
253                let mut first_section = true;
254                for (section, val) in map {
255                    if let Value::Object(section_map) = val {
256                        if !first_section {
257                            output.push('\n');
258                        }
259                        first_section = false;
260                        output.push_str(&format!("[{}]\n", section));
261                        for (key, v) in section_map {
262                            output.push_str(&format!("{} = {}\n", key, Self::format_value(v)));
263                        }
264                    }
265                }
266
267                Ok(output)
268            }
269            _ => {
270                anyhow::bail!(
271                    "INI format requires an Object value (sections with key-value pairs). \
272                     Got: {}",
273                    match value {
274                        Value::Null => "null",
275                        Value::Bool(_) => "boolean",
276                        Value::Integer(_) => "integer",
277                        Value::Float(_) => "float",
278                        Value::String(_) => "string",
279                        Value::Array(_) => "array",
280                        _ => "unknown",
281                    }
282                );
283            }
284        }
285    }
286
287    fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
288        let output = self.write(value)?;
289        writer
290            .write_all(output.as_bytes())
291            .map_err(|e| crate::error::DkitError::WriteError {
292                format: "INI".to_string(),
293                source: Box::new(e),
294            })?;
295        Ok(())
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    // --- IniReader 테스트 ---
304
305    #[test]
306    fn test_reader_simple_section() {
307        let reader = IniReader;
308        let input = "[database]\nhost = localhost\nport = 5432\n";
309        let v = reader.read(input).unwrap();
310        let obj = v.as_object().unwrap();
311        let db = obj.get("database").unwrap().as_object().unwrap();
312        assert_eq!(
313            db.get("host"),
314            Some(&Value::String("localhost".to_string()))
315        );
316        assert_eq!(db.get("port"), Some(&Value::Integer(5432)));
317    }
318
319    #[test]
320    fn test_reader_multiple_sections() {
321        let reader = IniReader;
322        let input = "[section1]\nkey1 = value1\n\n[section2]\nkey2 = value2\n";
323        let v = reader.read(input).unwrap();
324        let obj = v.as_object().unwrap();
325        assert_eq!(obj.len(), 2);
326
327        let s1 = obj.get("section1").unwrap().as_object().unwrap();
328        assert_eq!(s1.get("key1"), Some(&Value::String("value1".to_string())));
329
330        let s2 = obj.get("section2").unwrap().as_object().unwrap();
331        assert_eq!(s2.get("key2"), Some(&Value::String("value2".to_string())));
332    }
333
334    #[test]
335    fn test_reader_comments_hash() {
336        let reader = IniReader;
337        let input = "# This is a comment\n[section]\nkey = value\n";
338        let v = reader.read(input).unwrap();
339        let obj = v.as_object().unwrap();
340        assert_eq!(obj.len(), 1);
341    }
342
343    #[test]
344    fn test_reader_comments_semicolon() {
345        let reader = IniReader;
346        let input = "; This is a comment\n[section]\nkey = value\n";
347        let v = reader.read(input).unwrap();
348        let obj = v.as_object().unwrap();
349        assert_eq!(obj.len(), 1);
350    }
351
352    #[test]
353    fn test_reader_empty_lines() {
354        let reader = IniReader;
355        let input = "[section]\n\nkey1 = val1\n\nkey2 = val2\n\n";
356        let v = reader.read(input).unwrap();
357        let section = v
358            .as_object()
359            .unwrap()
360            .get("section")
361            .unwrap()
362            .as_object()
363            .unwrap();
364        assert_eq!(section.len(), 2);
365    }
366
367    #[test]
368    fn test_reader_colon_separator() {
369        let reader = IniReader;
370        let input = "[section]\nkey: value\n";
371        let v = reader.read(input).unwrap();
372        let section = v
373            .as_object()
374            .unwrap()
375            .get("section")
376            .unwrap()
377            .as_object()
378            .unwrap();
379        assert_eq!(
380            section.get("key"),
381            Some(&Value::String("value".to_string()))
382        );
383    }
384
385    #[test]
386    fn test_reader_no_section_top_level() {
387        let reader = IniReader;
388        let input = "key1 = value1\nkey2 = value2\n";
389        let v = reader.read(input).unwrap();
390        let obj = v.as_object().unwrap();
391        assert_eq!(obj.get("key1"), Some(&Value::String("value1".to_string())));
392        assert_eq!(obj.get("key2"), Some(&Value::String("value2".to_string())));
393    }
394
395    #[test]
396    fn test_reader_mixed_top_level_and_sections() {
397        let reader = IniReader;
398        let input = "global_key = global_value\n\n[section]\nkey = value\n";
399        let v = reader.read(input).unwrap();
400        let obj = v.as_object().unwrap();
401        assert_eq!(
402            obj.get("global_key"),
403            Some(&Value::String("global_value".to_string()))
404        );
405        let section = obj.get("section").unwrap().as_object().unwrap();
406        assert_eq!(
407            section.get("key"),
408            Some(&Value::String("value".to_string()))
409        );
410    }
411
412    #[test]
413    fn test_reader_boolean_values() {
414        let reader = IniReader;
415        let input = "[flags]\nenabled = true\ndisabled = false\nyes_val = yes\nno_val = no\non_val = on\noff_val = off\n";
416        let v = reader.read(input).unwrap();
417        let flags = v
418            .as_object()
419            .unwrap()
420            .get("flags")
421            .unwrap()
422            .as_object()
423            .unwrap();
424        assert_eq!(flags.get("enabled"), Some(&Value::Bool(true)));
425        assert_eq!(flags.get("disabled"), Some(&Value::Bool(false)));
426        assert_eq!(flags.get("yes_val"), Some(&Value::Bool(true)));
427        assert_eq!(flags.get("no_val"), Some(&Value::Bool(false)));
428        assert_eq!(flags.get("on_val"), Some(&Value::Bool(true)));
429        assert_eq!(flags.get("off_val"), Some(&Value::Bool(false)));
430    }
431
432    #[test]
433    fn test_reader_integer_values() {
434        let reader = IniReader;
435        let input = "[numbers]\nport = 8080\nnegative = -42\nzero = 0\n";
436        let v = reader.read(input).unwrap();
437        let nums = v
438            .as_object()
439            .unwrap()
440            .get("numbers")
441            .unwrap()
442            .as_object()
443            .unwrap();
444        assert_eq!(nums.get("port"), Some(&Value::Integer(8080)));
445        assert_eq!(nums.get("negative"), Some(&Value::Integer(-42)));
446        assert_eq!(nums.get("zero"), Some(&Value::Integer(0)));
447    }
448
449    #[test]
450    fn test_reader_float_values() {
451        let reader = IniReader;
452        let input = "[numbers]\npi = 3.14\nrate = 0.5\n";
453        let v = reader.read(input).unwrap();
454        let nums = v
455            .as_object()
456            .unwrap()
457            .get("numbers")
458            .unwrap()
459            .as_object()
460            .unwrap();
461        assert_eq!(nums.get("pi"), Some(&Value::Float(3.14)));
462        assert_eq!(nums.get("rate"), Some(&Value::Float(0.5)));
463    }
464
465    #[test]
466    fn test_reader_quoted_string() {
467        let reader = IniReader;
468        let input = "[section]\nname = \"hello world\"\nsingle = 'quoted'\n";
469        let v = reader.read(input).unwrap();
470        let section = v
471            .as_object()
472            .unwrap()
473            .get("section")
474            .unwrap()
475            .as_object()
476            .unwrap();
477        assert_eq!(
478            section.get("name"),
479            Some(&Value::String("hello world".to_string()))
480        );
481        assert_eq!(
482            section.get("single"),
483            Some(&Value::String("quoted".to_string()))
484        );
485    }
486
487    #[test]
488    fn test_reader_empty_value() {
489        let reader = IniReader;
490        let input = "[section]\nempty =\n";
491        let v = reader.read(input).unwrap();
492        let section = v
493            .as_object()
494            .unwrap()
495            .get("section")
496            .unwrap()
497            .as_object()
498            .unwrap();
499        assert_eq!(section.get("empty"), Some(&Value::String(String::new())));
500    }
501
502    #[test]
503    fn test_reader_inline_comment() {
504        let reader = IniReader;
505        let input = "[section]\nkey = value ; this is a comment\n";
506        let v = reader.read(input).unwrap();
507        let section = v
508            .as_object()
509            .unwrap()
510            .get("section")
511            .unwrap()
512            .as_object()
513            .unwrap();
514        assert_eq!(
515            section.get("key"),
516            Some(&Value::String("value".to_string()))
517        );
518    }
519
520    #[test]
521    fn test_reader_inline_comment_hash() {
522        let reader = IniReader;
523        let input = "[section]\nkey = value # this is a comment\n";
524        let v = reader.read(input).unwrap();
525        let section = v
526            .as_object()
527            .unwrap()
528            .get("section")
529            .unwrap()
530            .as_object()
531            .unwrap();
532        assert_eq!(
533            section.get("key"),
534            Some(&Value::String("value".to_string()))
535        );
536    }
537
538    #[test]
539    fn test_reader_value_with_equals() {
540        let reader = IniReader;
541        let input = "[section]\nurl = https://example.com?key=value\n";
542        let v = reader.read(input).unwrap();
543        let section = v
544            .as_object()
545            .unwrap()
546            .get("section")
547            .unwrap()
548            .as_object()
549            .unwrap();
550        assert_eq!(
551            section.get("url"),
552            Some(&Value::String("https://example.com?key=value".to_string()))
553        );
554    }
555
556    #[test]
557    fn test_reader_whitespace_around_key_value() {
558        let reader = IniReader;
559        let input = "[section]\n  key  =  value  \n";
560        let v = reader.read(input).unwrap();
561        let section = v
562            .as_object()
563            .unwrap()
564            .get("section")
565            .unwrap()
566            .as_object()
567            .unwrap();
568        assert_eq!(
569            section.get("key"),
570            Some(&Value::String("value".to_string()))
571        );
572    }
573
574    #[test]
575    fn test_reader_empty_input() {
576        let reader = IniReader;
577        let v = reader.read("").unwrap();
578        assert!(v.as_object().unwrap().is_empty());
579    }
580
581    #[test]
582    fn test_reader_only_comments() {
583        let reader = IniReader;
584        let input = "# comment 1\n; comment 2\n";
585        let v = reader.read(input).unwrap();
586        assert!(v.as_object().unwrap().is_empty());
587    }
588
589    #[test]
590    fn test_reader_duplicate_keys_last_wins() {
591        let reader = IniReader;
592        let input = "[section]\nkey = first\nkey = second\n";
593        let v = reader.read(input).unwrap();
594        let section = v
595            .as_object()
596            .unwrap()
597            .get("section")
598            .unwrap()
599            .as_object()
600            .unwrap();
601        assert_eq!(
602            section.get("key"),
603            Some(&Value::String("second".to_string()))
604        );
605    }
606
607    #[test]
608    fn test_reader_unclosed_section_error() {
609        let reader = IniReader;
610        let input = "[section\nkey = value\n";
611        let result = reader.read(input);
612        assert!(result.is_err());
613    }
614
615    #[test]
616    fn test_reader_empty_section_name_error() {
617        let reader = IniReader;
618        let input = "[]\nkey = value\n";
619        let result = reader.read(input);
620        assert!(result.is_err());
621    }
622
623    #[test]
624    fn test_reader_no_separator_error() {
625        let reader = IniReader;
626        let input = "[section]\ninvalid line\n";
627        let result = reader.read(input);
628        assert!(result.is_err());
629    }
630
631    #[test]
632    fn test_reader_from_reader() {
633        let reader = IniReader;
634        let input = b"[section]\nkey = value" as &[u8];
635        let v = reader.read_from_reader(input).unwrap();
636        let section = v
637            .as_object()
638            .unwrap()
639            .get("section")
640            .unwrap()
641            .as_object()
642            .unwrap();
643        assert_eq!(
644            section.get("key"),
645            Some(&Value::String("value".to_string()))
646        );
647    }
648
649    #[test]
650    fn test_reader_section_with_spaces() {
651        let reader = IniReader;
652        let input = "[ my section ]\nkey = value\n";
653        let v = reader.read(input).unwrap();
654        assert!(v.as_object().unwrap().contains_key("my section"));
655    }
656
657    #[test]
658    fn test_reader_case_sensitive_boolean() {
659        let reader = IniReader;
660        let input = "[section]\na = True\nb = FALSE\nc = Yes\nd = NO\n";
661        let v = reader.read(input).unwrap();
662        let section = v
663            .as_object()
664            .unwrap()
665            .get("section")
666            .unwrap()
667            .as_object()
668            .unwrap();
669        assert_eq!(section.get("a"), Some(&Value::Bool(true)));
670        assert_eq!(section.get("b"), Some(&Value::Bool(false)));
671        assert_eq!(section.get("c"), Some(&Value::Bool(true)));
672        assert_eq!(section.get("d"), Some(&Value::Bool(false)));
673    }
674
675    // --- IniWriter 테스트 ---
676
677    #[test]
678    fn test_writer_simple_section() {
679        let writer = IniWriter;
680        let v = Value::Object({
681            let mut m = IndexMap::new();
682            let mut section = IndexMap::new();
683            section.insert("host".to_string(), Value::String("localhost".to_string()));
684            section.insert("port".to_string(), Value::Integer(5432));
685            m.insert("database".to_string(), Value::Object(section));
686            m
687        });
688        let output = writer.write(&v).unwrap();
689        assert!(output.contains("[database]"));
690        assert!(output.contains("host = localhost"));
691        assert!(output.contains("port = 5432"));
692    }
693
694    #[test]
695    fn test_writer_multiple_sections() {
696        let writer = IniWriter;
697        let v = Value::Object({
698            let mut m = IndexMap::new();
699            let mut s1 = IndexMap::new();
700            s1.insert("key1".to_string(), Value::String("val1".to_string()));
701            m.insert("section1".to_string(), Value::Object(s1));
702            let mut s2 = IndexMap::new();
703            s2.insert("key2".to_string(), Value::String("val2".to_string()));
704            m.insert("section2".to_string(), Value::Object(s2));
705            m
706        });
707        let output = writer.write(&v).unwrap();
708        assert!(output.contains("[section1]"));
709        assert!(output.contains("[section2]"));
710        assert!(output.contains("key1 = val1"));
711        assert!(output.contains("key2 = val2"));
712    }
713
714    #[test]
715    fn test_writer_top_level_keys() {
716        let writer = IniWriter;
717        let v = Value::Object({
718            let mut m = IndexMap::new();
719            m.insert("global".to_string(), Value::String("value".to_string()));
720            m
721        });
722        let output = writer.write(&v).unwrap();
723        assert_eq!(output, "global = value\n");
724    }
725
726    #[test]
727    fn test_writer_mixed_top_level_and_sections() {
728        let writer = IniWriter;
729        let v = Value::Object({
730            let mut m = IndexMap::new();
731            m.insert("global".to_string(), Value::String("value".to_string()));
732            let mut section = IndexMap::new();
733            section.insert("key".to_string(), Value::String("val".to_string()));
734            m.insert("section".to_string(), Value::Object(section));
735            m
736        });
737        let output = writer.write(&v).unwrap();
738        assert!(output.starts_with("global = value\n"));
739        assert!(output.contains("[section]"));
740        assert!(output.contains("key = val"));
741    }
742
743    #[test]
744    fn test_writer_boolean() {
745        let writer = IniWriter;
746        let v = Value::Object({
747            let mut m = IndexMap::new();
748            let mut section = IndexMap::new();
749            section.insert("enabled".to_string(), Value::Bool(true));
750            section.insert("disabled".to_string(), Value::Bool(false));
751            m.insert("flags".to_string(), Value::Object(section));
752            m
753        });
754        let output = writer.write(&v).unwrap();
755        assert!(output.contains("enabled = true"));
756        assert!(output.contains("disabled = false"));
757    }
758
759    #[test]
760    fn test_writer_null_value() {
761        let writer = IniWriter;
762        let v = Value::Object({
763            let mut m = IndexMap::new();
764            let mut section = IndexMap::new();
765            section.insert("empty".to_string(), Value::Null);
766            m.insert("section".to_string(), Value::Object(section));
767            m
768        });
769        let output = writer.write(&v).unwrap();
770        assert!(output.contains("empty = \n"));
771    }
772
773    #[test]
774    fn test_writer_quotes_special_chars() {
775        let writer = IniWriter;
776        let v = Value::Object({
777            let mut m = IndexMap::new();
778            let mut section = IndexMap::new();
779            section.insert(
780                "val".to_string(),
781                Value::String("has;semicolon".to_string()),
782            );
783            m.insert("section".to_string(), Value::Object(section));
784            m
785        });
786        let output = writer.write(&v).unwrap();
787        assert!(output.contains("val = \"has;semicolon\""));
788    }
789
790    #[test]
791    fn test_writer_empty_string() {
792        let writer = IniWriter;
793        let v = Value::Object({
794            let mut m = IndexMap::new();
795            let mut section = IndexMap::new();
796            section.insert("val".to_string(), Value::String(String::new()));
797            m.insert("section".to_string(), Value::Object(section));
798            m
799        });
800        let output = writer.write(&v).unwrap();
801        assert!(output.contains("val = \"\""));
802    }
803
804    #[test]
805    fn test_writer_non_object_error() {
806        let writer = IniWriter;
807        let result = writer.write(&Value::String("hello".to_string()));
808        assert!(result.is_err());
809    }
810
811    #[test]
812    fn test_writer_to_writer() {
813        let writer = IniWriter;
814        let v = Value::Object({
815            let mut m = IndexMap::new();
816            let mut section = IndexMap::new();
817            section.insert("key".to_string(), Value::String("value".to_string()));
818            m.insert("section".to_string(), Value::Object(section));
819            m
820        });
821        let mut buf = Vec::new();
822        writer.write_to_writer(&v, &mut buf).unwrap();
823        let output = String::from_utf8(buf).unwrap();
824        assert!(output.contains("[section]"));
825        assert!(output.contains("key = value"));
826    }
827
828    // --- 왕복 변환 테스트 ---
829
830    #[test]
831    fn test_roundtrip_simple() {
832        let input = "[database]\nhost = localhost\nport = 5432\n";
833        let reader = IniReader;
834        let writer = IniWriter;
835
836        let value = reader.read(input).unwrap();
837        let output = writer.write(&value).unwrap();
838        let value2 = reader.read(&output).unwrap();
839
840        assert_eq!(value, value2);
841    }
842
843    #[test]
844    fn test_roundtrip_multiple_sections() {
845        let input =
846            "[server]\nhost = 0.0.0.0\nport = 8080\n\n[database]\nhost = localhost\nport = 5432\n";
847        let reader = IniReader;
848        let writer = IniWriter;
849
850        let value = reader.read(input).unwrap();
851        let output = writer.write(&value).unwrap();
852        let value2 = reader.read(&output).unwrap();
853
854        assert_eq!(value, value2);
855    }
856
857    #[test]
858    fn test_roundtrip_mixed() {
859        let input = "global = value\n\n[section]\nkey = val\n";
860        let reader = IniReader;
861        let writer = IniWriter;
862
863        let value = reader.read(input).unwrap();
864        let output = writer.write(&value).unwrap();
865        let value2 = reader.read(&output).unwrap();
866
867        assert_eq!(value, value2);
868    }
869
870    #[test]
871    fn test_roundtrip_booleans_and_numbers() {
872        let input = "[config]\nenabled = true\ncount = 42\nrate = 3.14\n";
873        let reader = IniReader;
874        let writer = IniWriter;
875
876        let value = reader.read(input).unwrap();
877        let output = writer.write(&value).unwrap();
878        let value2 = reader.read(&output).unwrap();
879
880        assert_eq!(value, value2);
881    }
882
883    #[test]
884    fn test_reader_empty_section() {
885        let reader = IniReader;
886        let input = "[empty]\n[notempty]\nkey = val\n";
887        let v = reader.read(input).unwrap();
888        let obj = v.as_object().unwrap();
889        assert!(obj.get("empty").unwrap().as_object().unwrap().is_empty());
890        assert_eq!(obj.get("notempty").unwrap().as_object().unwrap().len(), 1);
891    }
892
893    #[test]
894    fn test_reader_realistic_config() {
895        let reader = IniReader;
896        let input = r#"
897; MySQL configuration file
898[mysqld]
899port = 3306
900bind-address = 127.0.0.1
901max_connections = 100
902innodb_buffer_pool_size = 256M
903
904[client]
905port = 3306
906socket = /var/run/mysqld/mysqld.sock
907"#;
908        let v = reader.read(input).unwrap();
909        let obj = v.as_object().unwrap();
910        let mysqld = obj.get("mysqld").unwrap().as_object().unwrap();
911        assert_eq!(mysqld.get("port"), Some(&Value::Integer(3306)));
912        assert_eq!(
913            mysqld.get("bind-address"),
914            Some(&Value::String("127.0.0.1".to_string()))
915        );
916        assert_eq!(mysqld.get("max_connections"), Some(&Value::Integer(100)));
917    }
918}