ini_doc/
lib.rs

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/// Wraps section and property values
18/// Extends to record their preceding documentation content
19/// Such as: blank lines, comment lines, etc.
20#[derive(Clone)]
21pub struct Document<T> {
22    // Each line of documentation content before the data line
23    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            // If there is no value, output nothing (for properties with only a key name)
83            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            // Print property's documentation content
123            for doc_line in &property_doc.doc_texts {
124                writeln!(f, "{}", doc_line)?;
125            }
126
127            // Print property
128            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    /// Create a new INI instance, preset a general section (None)
173    pub fn new() -> Self {
174        let mut sections = Map::new();
175        // Preset a general section
176        sections.insert(None, Properties::new().into());
177        Self { sections }
178    }
179
180    /// Ensure a named section exists in the INI document.
181    ///
182    /// If the section does not exist, it will be created. The general section (None) cannot be created explicitly.
183    ///
184    /// # Arguments
185    /// * `section_name` - The name of the section to ensure exists.
186    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    /// Set a property value in a section.
194    ///
195    /// The section must already exist. If the property does not exist, it will be created; if it exists, it will be overwritten.
196    /// If `value` is `Some`, sets the key-value pair; if `None`, sets only the key name (no value).
197    ///
198    /// # Arguments
199    /// * `section_name` - The section name (None for general section).
200    /// * `key` - The property key.
201    /// * `value` - The property value (optional).
202    ///
203    /// # Errors
204    /// Returns `ConfigError::SectionNotFound` if the section does not exist.
205    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        // Get section, return error if not exists
214        let section_doc =
215            self.sections
216                .get_mut(&section_key)
217                .ok_or_else(|| ConfigError::SectionNotFound {
218                    section: section_name.map(|s| s.to_string()),
219                })?;
220
221        // Set property
222        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    /// Set documentation comments for a section.
231    ///
232    /// # Arguments
233    /// * `section_name` - The section name (None for general section).
234    /// * `doc_texts` - Documentation lines to associate with the section.
235    ///
236    /// # Errors
237    /// Returns `ConfigError::SectionNotFound` if the section does not exist.
238    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        // Get section, error if not exists
246        let section_doc =
247            self.sections
248                .get_mut(&section_key)
249                .ok_or_else(|| ConfigError::SectionNotFound {
250                    section: section_name.map(|s| s.to_string()),
251                })?;
252
253        // Update documentation
254        section_doc.doc_texts = doc_texts;
255        Ok(())
256    }
257
258    /// Set documentation comments for a property.
259    ///
260    /// # Arguments
261    /// * `section_name` - The section name (None for general section).
262    /// * `key` - The property key.
263    /// * `doc_texts` - Documentation lines to associate with the property.
264    ///
265    /// # Errors
266    /// Returns `ConfigError::SectionNotFound` if the section does not exist, or `ConfigError::PropertyNotFound` if the property does not exist.
267    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        // Get section
276        let section_doc =
277            self.sections
278                .get_mut(&section_key)
279                .ok_or_else(|| ConfigError::SectionNotFound {
280                    section: section_name.map(|s| s.to_string()),
281                })?;
282
283        // Get property
284        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        // Update property documentation
295        property_doc.doc_texts = doc_texts;
296        Ok(())
297    }
298
299    /// Get the value of the specified section and key, parse to the specified type via FromStr
300    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(&section_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                    // Only key name, no value
313                    return Ok(None);
314                }
315            }
316        }
317        Ok(None)
318    }
319
320    /// Get the raw string value of the specified section and key
321    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(&section_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    /// Check if the specified property exists
333    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(&section_key) {
337            return section_doc.data.contains_key(key);
338        }
339        false
340    }
341
342    /// Remove the specified property
343    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(&section_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    /// Remove the specified section
363    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(&section_key).is_some()
368        }
369        #[cfg(not(feature = "ordered"))]
370        {
371            self.sections.remove(&section_key).is_some()
372        }
373    }
374
375    /// Get all sections
376    pub fn sections(&self) -> &Map<SectionKey, SectionDocument> {
377        &self.sections
378    }
379
380    /// Get the specified section
381    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        // First handle the global section (no name)
390        // std::collections::HashMap orders are not guaranteed, so we handle the global section first
391        if let Some(global_section) = self.sections.get(&None) {
392            // Print documentation content of the global section
393            for doc_line in &global_section.doc_texts {
394                writeln!(f, "{}", doc_line)?;
395            }
396            // Print properties of the global section
397            write!(f, "{}", global_section.data)?;
398        }
399
400        // Then handle named sections
401        for (section_key, section_doc) in &self.sections {
402            if section_key.is_none() {
403                continue; // Global section already handled
404            }
405
406            // Print documentation content of the section
407            for doc_line in &section_doc.doc_texts {
408                writeln!(f, "{}", doc_line)?;
409            }
410
411            // Print section header
412            if let Some(section_name) = section_key {
413                writeln!(f, "[{}]", section_name)?;
414            }
415
416            // Print properties of the section
417            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                        // Documentation for the global section
456                        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                    // Set property documentation
466                    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                    // SectionEnd does not need special handling
473                }
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        // For debugging the structure of Item
491        let content = r#"
492; This is a comment
493[section1]
494; Property comment
495key1=value1
496
497[section2]
498key2=value2
499"#;
500
501        // Let's see how ini_roundtrip parses
502        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        // Verify that parsing the output again does not fail
556        let _ini2: Ini = result.parse().expect("second parse failed");
557
558        println!("Original content:\n{}", content);
559        println!("Parsed result:\n{}", result);
560
561        // Verify expected sections and properties
562        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        // Test the Display implementation of Document<T>
589        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        // Test the Display of Properties
608        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        // Test the Display of SectionDocument
616        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        // Test original content preservation
628        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        // Verify original comment format is preserved
647        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")); // Note: indentation may not be preserved
652
653        // Verify property values are correct
654        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        // Test setting section and properties
664        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(); // Only key name, no value
675
676        // Test getting values
677        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); // 只有键名,没有值
684
685        // Test if property exists
686        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        // Test overwriting property
691        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        // Test setting documentation
696        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        // Verify documentation settings
714        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        // 添加一些数据
725        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        // 测试删除属性
736        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        // 测试删除不存在的属性
741        assert!(!ini.remove_property(Some("section1"), "nonexistent"));
742
743        // 测试删除 section
744        assert!(ini.remove_section(Some("section2")));
745        assert!(!ini.has_property(Some("section2"), "key3"));
746
747        // 测试删除不存在的 section
748        assert!(!ini.remove_section(Some("nonexistent")));
749    }
750
751    #[test]
752    fn test_type_conversion() {
753        let mut ini = Ini::new();
754
755        // 设置不同类型的值
756        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        // 测试类型转换
768        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        // 测试类型转换失败
786        assert!(ini.get_value::<i32>(Some("config"), "name").is_err());
787
788        // 测试不存在的值
789        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        // (Removed duplicated INI content block)
798        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        // Modify existing configuration
810        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(); // Add new property
815        ini.set_property(Some("cache"), "enabled", Some(true))
816            .unwrap(); // Add new section
817
818        // Modify documentation
819        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        // Verify modification results
830        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        // Test that setting documentation for a nonexistent section should fail
844        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        // Test that setting documentation for a nonexistent property should fail
857        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        // Test that setting documentation for a property in a nonexistent section should fail
875        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        // Test correct case
889        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        // Test that setting a property in a nonexistent section should fail
908        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        // Test that setting a property in the global section (None) should now succeed, as the global section exists by default
918        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        // Correct workflow: create section first, then set property
925        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        // Verify values are set correctly
937        assert_eq!(ini.get_string(Some("test"), "key1"), Some("value1"));
938        assert_eq!(ini.get_string(None, "global_key"), Some("value")); // Previously set
939        assert_eq!(
940            ini.get_string(None, "another_global_key"),
941            Some("another_global_value")
942        );
943
944        // Test idempotency of set_section (calling it multiple times for the same section should not fail)
945        ini.set_section("test"); // Set the same section again
946        assert!(
947            ini.set_property(Some("test"), "key2", Some("value2"))
948                .is_ok()
949        );
950    }
951}