1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatReader, FormatWriter};
6use crate::value::Value;
7
8pub struct IniReader;
19
20impl IniReader {
21 fn parse_value(raw: &str) -> Value {
23 let trimmed = raw.trim();
24
25 if trimmed.is_empty() {
27 return Value::String(String::new());
28 }
29
30 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 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 if let Ok(n) = trimmed.parse::<i64>() {
47 return Value::Integer(n);
48 }
49
50 if let Ok(f) = trimmed.parse::<f64>() {
52 return Value::Float(f);
53 }
54
55 Value::String(trimmed.to_string())
56 }
57
58 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 if i > 0 && value_str.as_bytes()[i - 1] == b' ' {
71 return &value_str[..i - 1];
72 }
73 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 if trimmed.is_empty() {
96 continue;
97 }
98
99 if trimmed.starts_with('#') || trimmed.starts_with(';') {
101 continue;
102 }
103
104 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 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 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 ¤t_section {
165 Some(section) => {
166 if let Some(Value::Object(section_map)) = root.get_mut(section) {
168 section_map.insert(key, value);
169 }
170 }
171 None => {
172 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
193pub struct IniWriter;
198
199impl IniWriter {
200 fn format_value(value: &Value) -> String {
202 match value {
203 Value::String(s) => {
204 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 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 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 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 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 #[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 #[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 #[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}