1use std::{
2 fmt::Display,
3 ops::{Deref, DerefMut},
4 str::FromStr,
5};
6
7extern crate ini_roundtrip as ini_engine;
8#[cfg(feature = "ordered")]
9use indexmap::IndexMap as Map;
10#[cfg(not(feature = "ordered"))]
11use std::collections::HashMap as Map;
12
13use thiserror::Error;
14
15use ini_engine::Item;
16
17#[derive(Clone)]
21pub struct Document<T> {
22 doc_texts: Vec<String>,
24 data: T,
25}
26
27impl<T> From<T> for Document<T> {
28 fn from(data: T) -> Self {
29 Self {
30 doc_texts: Vec::new(),
31 data,
32 }
33 }
34}
35
36impl<T> Document<T> {
37 pub fn new(data: T, doc_texts: Vec<String>) -> Self {
38 Self { doc_texts, data }
39 }
40
41 pub fn doc_texts(&self) -> &[String] {
42 &self.doc_texts
43 }
44}
45
46impl<T: Display> Display for Document<T> {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 for doc_line in &self.doc_texts {
49 writeln!(f, "{}", doc_line)?;
50 }
51 write!(f, "{}", self.data)
52 }
53}
54
55impl<T> DerefMut for Document<T> {
56 fn deref_mut(&mut self) -> &mut Self::Target {
57 &mut self.data
58 }
59}
60
61impl<T> Deref for Document<T> {
62 type Target = T;
63
64 fn deref(&self) -> &Self::Target {
65 &self.data
66 }
67}
68
69pub type SectionKey = Option<String>;
70pub type PropertyKey = String;
71
72#[derive(Clone)]
73pub struct PropertyValue {
74 pub value: Option<String>,
75}
76
77impl Display for PropertyValue {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 if let Some(value) = &self.value {
80 write!(f, "{}", value)
81 } else {
82 Ok(())
84 }
85 }
86}
87
88#[derive(Clone)]
89pub struct Properties {
90 inner: Map<String, PropertyDocument>,
91}
92
93impl Properties {
94 pub fn new() -> Self {
95 Self { inner: Map::new() }
96 }
97
98 pub fn insert(
99 &mut self,
100 key: PropertyKey,
101 value: PropertyDocument,
102 ) -> Option<PropertyDocument> {
103 self.inner.insert(key, value)
104 }
105
106 pub fn get(&self, key: &str) -> Option<&PropertyDocument> {
107 self.inner.get(key)
108 }
109
110 pub fn contains_key(&self, key: &str) -> bool {
111 self.inner.contains_key(key)
112 }
113
114 pub fn is_empty(&self) -> bool {
115 self.inner.is_empty()
116 }
117}
118
119impl Display for Properties {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 for (property_key, property_doc) in &self.inner {
122 for doc_line in &property_doc.doc_texts {
124 writeln!(f, "{}", doc_line)?;
125 }
126
127 if let Some(value) = &property_doc.value {
129 writeln!(f, "{}={}", property_key, value)?;
130 } else {
131 writeln!(f, "{}", property_key)?;
132 }
133 }
134 Ok(())
135 }
136}
137
138pub type PropertyDocument = Document<PropertyValue>;
139
140pub type SectionDocument = Document<Properties>;
141
142#[derive(Error, Debug)]
143pub enum ParseError {
144 #[error("parse error: {0}")]
145 FailedParse(String),
146}
147
148#[derive(Error, Debug)]
149pub enum ConfigError {
150 #[error("section {section:?} not found")]
151 SectionNotFound { section: Option<String> },
152 #[error("property '{property}' not found in section {section:?}")]
153 PropertyNotFound {
154 section: Option<String>,
155 property: String,
156 },
157}
158
159#[derive(Error, Debug)]
160pub enum IniError {
161 #[error(transparent)]
162 Parse(#[from] ParseError),
163 #[error(transparent)]
164 Config(#[from] ConfigError),
165}
166
167pub struct Ini {
168 sections: Map<SectionKey, SectionDocument>,
169}
170
171impl Ini {
172 pub fn new() -> Self {
174 let mut sections = Map::new();
175 sections.insert(None, Properties::new().into());
177 Self { sections }
178 }
179
180 pub fn set_section(&mut self, section_name: &str) {
187 let section_key = Some(section_name.to_string());
188 self.sections
189 .entry(section_key)
190 .or_insert_with(|| Properties::new().into());
191 }
192
193 pub fn set_property<T: ToString>(
206 &mut self,
207 section_name: Option<&str>,
208 key: &str,
209 value: Option<T>,
210 ) -> Result<(), ConfigError> {
211 let section_key = section_name.map(|s| s.to_string());
212
213 let section_doc =
215 self.sections
216 .get_mut(§ion_key)
217 .ok_or_else(|| ConfigError::SectionNotFound {
218 section: section_name.map(|s| s.to_string()),
219 })?;
220
221 let property_value = PropertyValue {
223 value: value.map(|v| v.to_string()),
224 };
225 section_doc.insert(key.to_string(), property_value.into());
226
227 Ok(())
228 }
229
230 pub fn set_section_doc(
239 &mut self,
240 section_name: Option<&str>,
241 doc_texts: Vec<String>,
242 ) -> Result<(), ConfigError> {
243 let section_key = section_name.map(|s| s.to_string());
244
245 let section_doc =
247 self.sections
248 .get_mut(§ion_key)
249 .ok_or_else(|| ConfigError::SectionNotFound {
250 section: section_name.map(|s| s.to_string()),
251 })?;
252
253 section_doc.doc_texts = doc_texts;
255 Ok(())
256 }
257
258 pub fn set_property_doc(
268 &mut self,
269 section_name: Option<&str>,
270 key: &str,
271 doc_texts: Vec<String>,
272 ) -> Result<(), ConfigError> {
273 let section_key = section_name.map(|s| s.to_string());
274
275 let section_doc =
277 self.sections
278 .get_mut(§ion_key)
279 .ok_or_else(|| ConfigError::SectionNotFound {
280 section: section_name.map(|s| s.to_string()),
281 })?;
282
283 let property_doc =
285 section_doc
286 .data
287 .inner
288 .get_mut(key)
289 .ok_or_else(|| ConfigError::PropertyNotFound {
290 section: section_name.map(|s| s.to_string()),
291 property: key.to_string(),
292 })?;
293
294 property_doc.doc_texts = doc_texts;
296 Ok(())
297 }
298
299 pub fn get_value<T: FromStr>(
301 &self,
302 section_name: Option<&str>,
303 key: &str,
304 ) -> Result<Option<T>, T::Err> {
305 let section_key = section_name.map(|s| s.to_string());
306
307 if let Some(section_doc) = self.sections.get(§ion_key) {
308 if let Some(property_doc) = section_doc.data.get(key) {
309 if let Some(value_str) = &property_doc.value {
310 return Ok(Some(value_str.parse()?));
311 } else {
312 return Ok(None);
314 }
315 }
316 }
317 Ok(None)
318 }
319
320 pub fn get_string(&self, section_name: Option<&str>, key: &str) -> Option<&str> {
322 let section_key = section_name.map(|s| s.to_string());
323
324 if let Some(section_doc) = self.sections.get(§ion_key) {
325 if let Some(property_doc) = section_doc.data.get(key) {
326 return property_doc.value.as_deref();
327 }
328 }
329 None
330 }
331
332 pub fn has_property(&self, section_name: Option<&str>, key: &str) -> bool {
334 let section_key = section_name.map(|s| s.to_string());
335
336 if let Some(section_doc) = self.sections.get(§ion_key) {
337 return section_doc.data.contains_key(key);
338 }
339 false
340 }
341
342 pub fn remove_property(&mut self, section_name: Option<&str>, key: &str) -> bool {
344 let section_key = section_name.map(|s| s.to_string());
345 match self.sections.get_mut(§ion_key) {
346 Some(section_doc) => {
347 #[cfg(feature = "ordered")]
348 {
349 let map = &mut section_doc.data.inner;
350 map.shift_remove(key).is_some()
351 }
352 #[cfg(not(feature = "ordered"))]
353 {
354 let map = &mut section_doc.data.inner;
355 map.remove(key).is_some()
356 }
357 }
358 None => false,
359 }
360 }
361
362 pub fn remove_section(&mut self, section_name: Option<&str>) -> bool {
364 let section_key = section_name.map(|s| s.to_string());
365 #[cfg(feature = "ordered")]
366 {
367 self.sections.shift_remove(§ion_key).is_some()
368 }
369 #[cfg(not(feature = "ordered"))]
370 {
371 self.sections.remove(§ion_key).is_some()
372 }
373 }
374
375 pub fn sections(&self) -> &Map<SectionKey, SectionDocument> {
377 &self.sections
378 }
379
380 pub fn section(&self, name: Option<&str>) -> Option<&SectionDocument> {
382 let key = name.map(|s| s.to_string());
383 self.sections.get(&key)
384 }
385}
386
387impl Display for Ini {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 if let Some(global_section) = self.sections.get(&None) {
392 for doc_line in &global_section.doc_texts {
394 writeln!(f, "{}", doc_line)?;
395 }
396 write!(f, "{}", global_section.data)?;
398 }
399
400 for (section_key, section_doc) in &self.sections {
402 if section_key.is_none() {
403 continue; }
405
406 for doc_line in §ion_doc.doc_texts {
408 writeln!(f, "{}", doc_line)?;
409 }
410
411 if let Some(section_name) = section_key {
413 writeln!(f, "[{}]", section_name)?;
414 }
415
416 write!(f, "{}", section_doc.data)?;
418 }
419 Ok(())
420 }
421}
422
423impl FromStr for Ini {
424 type Err = IniError;
425
426 fn from_str(s: &str) -> Result<Self, Self::Err> {
427 let items: Vec<Item> = ini_engine::Parser::new(s).collect();
428 Self::try_from(items)
429 }
430}
431
432impl TryFrom<Vec<Item<'_>>> for Ini {
433 type Error = IniError;
434
435 fn try_from(value: Vec<Item<'_>>) -> Result<Self, Self::Error> {
436 let mut ini = Ini::new();
437 let mut current_section: Option<String> = None;
438 let mut pending_docs: Vec<String> = Vec::new();
439
440 for item in value {
441 match item {
442 Item::Blank { raw } => {
443 pending_docs.push(raw.to_string());
444 }
445 Item::Comment { raw, .. } => {
446 pending_docs.push(raw.to_string());
447 }
448 Item::Section { name, raw: _ } => {
449 if let Some(ref sec) = current_section {
450 if !pending_docs.is_empty() {
451 let docs: Vec<String> = pending_docs.drain(..).collect();
452 ini.set_section_doc(Some(sec), docs).ok();
453 }
454 } else if !pending_docs.is_empty() {
455 let docs: Vec<String> = pending_docs.drain(..).collect();
457 ini.set_section_doc(None, docs).ok();
458 }
459 ini.set_section(&name);
460 current_section = Some(name.to_string());
461 }
462 Item::Property { key, val, raw: _ } => {
463 let section = current_section.as_deref();
464 ini.set_property(section, &key, val.map(|v| v.to_string()))?;
465 if !pending_docs.is_empty() {
467 let docs: Vec<String> = pending_docs.drain(..).collect();
468 ini.set_property_doc(section, &key, docs).ok();
469 }
470 }
471 Item::SectionEnd => {
472 }
474 Item::Error(err) => {
475 return Err(ParseError::FailedParse(err.to_string()).into());
476 }
477 }
478 }
479
480 Ok(ini)
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn test_item_structure() {
490 let content = r#"
492; This is a comment
493[section1]
494; Property comment
495key1=value1
496
497[section2]
498key2=value2
499"#;
500
501 let items = ini_engine::Parser::new(content).collect::<Vec<_>>();
503 for item in items {
504 println!("{:?}", item);
505 }
506 }
507
508 #[test]
509 fn test_parse_and_display() {
510 let content = r#"
511; Global comment
512global_key=global_value
513
514; section1 comment
515[section1]
516; key1 comment
517key1=value1
518
519; key2 comment
520key2=value2
521
522[section2]
523key3=value3
524"#;
525
526 let ini: Ini = content.parse().expect("解析失败");
527 let result = ini.to_string();
528 println!("解析结果:\n{}", result);
529 }
530
531 #[test]
532 fn test_round_trip() {
533 let content = r#"; Config file header comment
534
535; Database config
536[database]
537; Host address
538host=localhost
539; Port number
540port=3306
541
542user=admin
543
544; Web service config
545[web]
546; Listen port
547listen_port=8080
548; Static directory
549static_dir=/var/www
550"#;
551
552 let ini: Ini = content.parse().expect("解析失败");
553 let result = ini.to_string();
554
555 let _ini2: Ini = result.parse().expect("second parse failed");
557
558 println!("Original content:\n{}", content);
559 println!("Parsed result:\n{}", result);
560
561 assert!(
563 ini.section(Some("database")).is_some(),
564 "should contain database section"
565 );
566 assert!(
567 ini.section(Some("web")).is_some(),
568 "should contain web section"
569 );
570
571 let db_section = ini.section(Some("database")).unwrap();
572 assert!(
573 db_section.contains_key("host"),
574 "database section should contain host"
575 );
576 assert!(
577 db_section.contains_key("port"),
578 "database section should contain port"
579 );
580 assert!(
581 db_section.contains_key("user"),
582 "database section should contain user"
583 );
584 }
585
586 #[test]
587 fn test_document_display() {
588 let property_value = PropertyValue {
590 value: Some("test_value".to_string()),
591 };
592
593 let doc_texts = vec![
594 "; This is a comment".to_string(),
595 String::new(),
596 "; Another comment".to_string(),
597 ];
598
599 let property_doc = Document::new(property_value, doc_texts);
600 let result = property_doc.to_string();
601
602 println!("PropertyDocument Display result:\n{}", result);
603 assert!(result.contains("; This is a comment"));
604 assert!(result.contains("; Another comment"));
605 assert!(result.contains("test_value"));
606
607 let mut properties = Properties::new();
609 properties.insert("key1".to_string(), property_doc);
610
611 let properties_result = properties.to_string();
612 println!("Properties Display 结果:\n{}", properties_result);
613 assert!(properties_result.contains("key1=test_value"));
614
615 let section_docs = vec!["; Section comment".to_string()];
617 let section_doc = Document::new(properties, section_docs);
618
619 let section_result = section_doc.to_string();
620 println!("SectionDocument Display result:\n{}", section_result);
621 assert!(section_result.contains("; Section comment"));
622 assert!(section_result.contains("key1=test_value"));
623 }
624
625 #[test]
626 fn test_original_content_preservation() {
627 let content = r#"; This is an original comment, with special format
629#This is a hash comment
630global_key=global_value
631
632; section1 comment, with spaces
633[section1]
634;no space comment
635key1=value1
636 ; indented comment
637key2=value2
638"#;
639
640 let ini: Ini = content.parse().expect("解析失败");
641 let result = ini.to_string();
642
643 println!("Original content:\n{}", content);
644 println!("Reconstructed result:\n{}", result);
645
646 assert!(result.contains("; This is an original comment, with special format"));
648 assert!(result.contains("#This is a hash comment"));
649 assert!(result.contains("; section1 comment, with spaces "));
650 assert!(result.contains(";no space comment"));
651 assert!(result.contains("; indented comment")); assert!(result.contains("global_key=global_value"));
655 assert!(result.contains("key1=value1"));
656 assert!(result.contains("key2=value2"));
657 }
658
659 #[test]
660 fn test_ini_editing() {
661 let mut ini = Ini::new();
662
663 ini.set_section("database");
665 ini.set_section("flags");
666
667 ini.set_property(Some("database"), "host", Some("localhost"))
668 .unwrap();
669 ini.set_property(Some("database"), "port", Some(3306))
670 .unwrap();
671 ini.set_property(None, "global_key", Some("global_value"))
672 .unwrap();
673 ini.set_property(Some("flags"), "debug", None::<String>)
674 .unwrap(); assert_eq!(ini.get_string(Some("database"), "host"), Some("localhost"));
678 assert_eq!(
679 ini.get_value::<i32>(Some("database"), "port").unwrap(),
680 Some(3306)
681 );
682 assert_eq!(ini.get_string(None, "global_key"), Some("global_value"));
683 assert_eq!(ini.get_string(Some("flags"), "debug"), None); assert!(ini.has_property(Some("database"), "host"));
687 assert!(ini.has_property(Some("flags"), "debug"));
688 assert!(!ini.has_property(Some("database"), "nonexistent"));
689
690 ini.set_property(Some("database"), "host", Some("127.0.0.1"))
692 .unwrap();
693 assert_eq!(ini.get_string(Some("database"), "host"), Some("127.0.0.1"));
694
695 ini.set_section_doc(
697 Some("database"),
698 vec![
699 "; Database configuration".to_string(),
700 "; Important configuration".to_string(),
701 ],
702 )
703 .unwrap();
704 ini.set_property_doc(
705 Some("database"),
706 "host",
707 vec!["; Database host address".to_string()],
708 )
709 .unwrap();
710
711 println!("编辑后的 INI:\n{}", ini);
712
713 let result = ini.to_string();
715 assert!(result.contains("; Database configuration"));
716 assert!(result.contains("; Important configuration"));
717 assert!(result.contains("; Database host address"));
718 }
719
720 #[test]
721 fn test_ini_deletion() {
722 let mut ini = Ini::new();
723
724 ini.set_section("section1");
726 ini.set_section("section2");
727
728 ini.set_property(Some("section1"), "key1", Some("value1"))
729 .unwrap();
730 ini.set_property(Some("section1"), "key2", Some("value2"))
731 .unwrap();
732 ini.set_property(Some("section2"), "key3", Some("value3"))
733 .unwrap();
734
735 assert!(ini.remove_property(Some("section1"), "key1"));
737 assert!(!ini.has_property(Some("section1"), "key1"));
738 assert!(ini.has_property(Some("section1"), "key2"));
739
740 assert!(!ini.remove_property(Some("section1"), "nonexistent"));
742
743 assert!(ini.remove_section(Some("section2")));
745 assert!(!ini.has_property(Some("section2"), "key3"));
746
747 assert!(!ini.remove_section(Some("nonexistent")));
749 }
750
751 #[test]
752 fn test_type_conversion() {
753 let mut ini = Ini::new();
754
755 ini.set_section("config");
757
758 ini.set_property(Some("config"), "port", Some(8080))
759 .unwrap();
760 ini.set_property(Some("config"), "timeout", Some(30.5))
761 .unwrap();
762 ini.set_property(Some("config"), "enabled", Some(true))
763 .unwrap();
764 ini.set_property(Some("config"), "name", Some("test_server"))
765 .unwrap();
766
767 assert_eq!(
769 ini.get_value::<i32>(Some("config"), "port").unwrap(),
770 Some(8080)
771 );
772 assert_eq!(
773 ini.get_value::<f64>(Some("config"), "timeout").unwrap(),
774 Some(30.5)
775 );
776 assert_eq!(
777 ini.get_value::<bool>(Some("config"), "enabled").unwrap(),
778 Some(true)
779 );
780 assert_eq!(
781 ini.get_value::<String>(Some("config"), "name").unwrap(),
782 Some("test_server".to_string())
783 );
784
785 assert!(ini.get_value::<i32>(Some("config"), "name").is_err());
787
788 assert_eq!(
790 ini.get_value::<i32>(Some("config"), "nonexistent").unwrap(),
791 None
792 );
793 }
794
795 #[test]
796 fn test_edit_existing_ini() {
797 let content = r#"; Original configuration
799[database]
800host=old_host
801port=3306
802
803[web]
804port=8080
805"#;
806
807 let mut ini: Ini = content.parse().expect("Parse failed");
808
809 ini.set_property(Some("database"), "host", Some("new_host"))
811 .unwrap();
812 ini.set_section("cache");
813 ini.set_property(Some("database"), "user", Some("admin"))
814 .unwrap(); ini.set_property(Some("cache"), "enabled", Some(true))
816 .unwrap(); ini.set_property_doc(
820 Some("database"),
821 "host",
822 vec![String::from("; New host address")],
823 )
824 .unwrap();
825
826 let result = ini.to_string();
827 println!("Modified configuration:\n{}", result);
828
829 assert_eq!(ini.get_string(Some("database"), "host"), Some("new_host"));
831 assert_eq!(ini.get_string(Some("database"), "user"), Some("admin"));
832 assert_eq!(
833 ini.get_value::<bool>(Some("cache"), "enabled").unwrap(),
834 Some(true)
835 );
836 assert!(result.contains("; New host address"));
837 }
838
839 #[test]
840 fn test_doc_validation() {
841 let mut ini = Ini::new();
842
843 let result = ini.set_section_doc(
845 Some("nonexistent"),
846 vec![String::from("; Nonexistent section")],
847 );
848 assert!(result.is_err());
849 match result.unwrap_err() {
850 ConfigError::SectionNotFound { section } => {
851 assert_eq!(section, Some("nonexistent".to_string()));
852 }
853 _ => panic!("Expected SectionNotFound error"),
854 }
855
856 ini.set_section("test");
858 ini.set_property(Some("test"), "key1", Some("value1"))
859 .unwrap();
860 let result = ini.set_property_doc(
861 Some("test"),
862 "nonexistent",
863 vec![String::from("; Nonexistent property")],
864 );
865 assert!(result.is_err());
866 match result.unwrap_err() {
867 ConfigError::PropertyNotFound { section, property } => {
868 assert_eq!(section, Some("test".to_string()));
869 assert_eq!(property, "nonexistent");
870 }
871 _ => panic!("Expected PropertyNotFound error"),
872 }
873
874 let result = ini.set_property_doc(
876 Some("nonexistent"),
877 "key1",
878 vec![String::from("; Nonexistent section")],
879 );
880 assert!(result.is_err());
881 match result.unwrap_err() {
882 ConfigError::SectionNotFound { section } => {
883 assert_eq!(section, Some("nonexistent".to_string()));
884 }
885 _ => panic!("Expected SectionNotFound error"),
886 }
887
888 assert!(
890 ini.set_section_doc(Some("test"), vec![String::from("; Test section")])
891 .is_ok()
892 );
893 assert!(
894 ini.set_property_doc(Some("test"), "key1", vec![String::from("; Test property")])
895 .is_ok()
896 );
897
898 let result = ini.to_string();
899 assert!(result.contains("; Test section"));
900 assert!(result.contains("; Test property"));
901 }
902
903 #[test]
904 fn test_strict_section_behavior() {
905 let mut ini = Ini::new();
906
907 let result = ini.set_property(Some("nonexistent"), "key", Some("value"));
909 assert!(result.is_err());
910 match result.unwrap_err() {
911 ConfigError::SectionNotFound { section } => {
912 assert_eq!(section, Some("nonexistent".to_string()));
913 }
914 _ => panic!("Expected SectionNotFound error"),
915 }
916
917 let result = ini.set_property(None, "global_key", Some("value"));
919 assert!(
920 result.is_ok(),
921 "Setting global property should succeed because the global section exists by default"
922 );
923
924 ini.set_section("test");
926
927 assert!(
928 ini.set_property(Some("test"), "key1", Some("value1"))
929 .is_ok()
930 );
931 assert!(
932 ini.set_property(None, "another_global_key", Some("another_global_value"))
933 .is_ok()
934 );
935
936 assert_eq!(ini.get_string(Some("test"), "key1"), Some("value1"));
938 assert_eq!(ini.get_string(None, "global_key"), Some("value")); assert_eq!(
940 ini.get_string(None, "another_global_key"),
941 Some("another_global_value")
942 );
943
944 ini.set_section("test"); assert!(
947 ini.set_property(Some("test"), "key2", Some("value2"))
948 .is_ok()
949 );
950 }
951}