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;
8use indexmap::IndexMap as Map;
9
10use thiserror::Error;
11
12use ini_engine::Item;
13
14/// Wraps section and property values after parsing
15/// Records their preceding documentation content and line numbers
16#[derive(Clone)]
17pub struct ParsedDocument<T> {
18    // Each line of documentation content before the data line
19    doc_texts: Vec<String>,
20    line_num: usize,
21    data: T,
22}
23
24/// Wraps section and property values for editing
25/// Extends to record their preceding documentation content
26#[derive(Clone)]
27pub struct EditableDocument<T> {
28    // Each line of documentation content before the data line
29    doc_texts: Vec<String>,
30    data: T,
31}
32
33/// For backward compatibility, keep the original Document type alias
34pub type Document<T> = EditableDocument<T>;
35
36impl<T> ParsedDocument<T> {
37    pub fn new(data: T, line_num: usize, doc_texts: Vec<String>) -> Self {
38        Self {
39            doc_texts,
40            line_num,
41            data,
42        }
43    }
44
45    pub fn doc_texts(&self) -> &[String] {
46        &self.doc_texts
47    }
48
49    pub fn line_num(&self) -> usize {
50        self.line_num
51    }
52
53    /// Convert to an editable structure by reference
54    pub fn to_editable(&self) -> EditableDocument<T>
55    where
56        T: Clone,
57    {
58        EditableDocument::new(self.data.clone(), self.doc_texts.clone())
59    }
60}
61
62impl<T> EditableDocument<T> {
63    pub fn new(data: T, doc_texts: Vec<String>) -> Self {
64        Self { doc_texts, data }
65    }
66
67    pub fn doc_texts(&self) -> &[String] {
68        &self.doc_texts
69    }
70
71    pub fn doc_texts_mut(&mut self) -> &mut Vec<String> {
72        &mut self.doc_texts
73    }
74
75    pub fn set_doc_texts(&mut self, doc_texts: Vec<String>) {
76        self.doc_texts = doc_texts;
77    }
78}
79
80impl<T> From<ParsedDocument<T>> for EditableDocument<T> {
81    fn from(parsed: ParsedDocument<T>) -> Self {
82        EditableDocument::new(parsed.data, parsed.doc_texts)
83    }
84}
85
86impl<T: Display> Display for ParsedDocument<T> {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        for doc_line in &self.doc_texts {
89            writeln!(f, "{}", doc_line)?;
90        }
91        write!(f, "{}", self.data)
92    }
93}
94
95impl<T: Display> Display for EditableDocument<T> {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        for doc_line in &self.doc_texts {
98            writeln!(f, "{}", doc_line)?;
99        }
100        write!(f, "{}", self.data)
101    }
102}
103
104impl<T> Deref for ParsedDocument<T> {
105    type Target = T;
106
107    fn deref(&self) -> &Self::Target {
108        &self.data
109    }
110}
111
112impl<T> DerefMut for EditableDocument<T> {
113    fn deref_mut(&mut self) -> &mut Self::Target {
114        &mut self.data
115    }
116}
117
118impl<T> Deref for EditableDocument<T> {
119    type Target = T;
120
121    fn deref(&self) -> &Self::Target {
122        &self.data
123    }
124}
125
126impl From<Properties> for EditableDocument<Properties> {
127    fn from(properties: Properties) -> Self {
128        EditableDocument::new(properties, vec![])
129    }
130}
131
132impl From<PropertyValue> for EditableDocument<PropertyValue> {
133    fn from(property_value: PropertyValue) -> Self {
134        EditableDocument::new(property_value, vec![])
135    }
136}
137
138pub type ParsedPropertyDocument = ParsedDocument<PropertyValue>;
139pub type ParsedSectionDocument = ParsedDocument<ReadonlyProperties>;
140
141pub type EditablePropertyDocument = EditableDocument<PropertyValue>;
142pub type EditableSectionDocument = EditableDocument<Properties>;
143
144pub type PropertyDocument = EditablePropertyDocument;
145pub type SectionDocument = EditableSectionDocument;
146
147pub type SectionKey = Option<String>;
148pub type PropertyKey = String;
149
150#[derive(Clone)]
151pub struct PropertyValue {
152    pub value: Option<String>,
153}
154
155impl Display for PropertyValue {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        if let Some(value) = &self.value {
158            write!(f, "{}", value)
159        } else {
160            // If there is no value, output nothing (for properties with only a key name)
161            Ok(())
162        }
163    }
164}
165
166#[derive(Clone)]
167pub struct Properties {
168    inner: Map<String, PropertyDocument>,
169}
170
171#[derive(Clone)]
172pub struct ReadonlyProperties {
173    inner: Map<String, ParsedPropertyDocument>,
174}
175
176impl ReadonlyProperties {
177    pub fn new() -> Self {
178        Self { inner: Map::new() }
179    }
180
181    pub fn get(&self, key: &str) -> Option<&ParsedPropertyDocument> {
182        self.inner.get(key)
183    }
184
185    pub fn iter(&self) -> impl Iterator<Item = (&String, &ParsedPropertyDocument)> {
186        self.inner.iter()
187    }
188
189    pub fn into_iter(self) -> impl Iterator<Item = (String, ParsedPropertyDocument)> {
190        self.inner.into_iter()
191    }
192}
193
194impl Properties {
195    pub fn new() -> Self {
196        Self { inner: Map::new() }
197    }
198
199    fn insert(&mut self, key: PropertyKey, value: PropertyDocument) -> Option<PropertyDocument> {
200        self.inner.insert(key, value)
201    }
202
203    pub fn get(&self, key: &str) -> Option<&PropertyDocument> {
204        self.inner.get(key)
205    }
206
207    pub fn get_mut(&mut self, key: &str) -> Option<&mut PropertyDocument> {
208        self.inner.get_mut(key)
209    }
210
211    pub fn get_value<T: FromStr>(&self, key: &str) -> Result<Option<T>, T::Err> {
212        if let Some(property_doc) = self.get(key) {
213            if let Some(value_str) = &property_doc.value {
214                return Ok(Some(value_str.parse()?));
215            } else {
216                // Only key name, no value
217                return Ok(None);
218            }
219        }
220        Ok(None)
221    }
222
223    pub fn iter(&self) -> impl Iterator<Item = (&String, &PropertyDocument)> {
224        self.inner.iter()
225    }
226
227    pub fn into_iter(self) -> impl Iterator<Item = (String, PropertyDocument)> {
228        self.inner.into_iter()
229    }
230
231    pub fn set(&mut self, key: PropertyKey, value: PropertyValue) -> Option<PropertyDocument> {
232        let value = EditableDocument::from(value);
233        self.insert(key, value)
234    }
235
236    pub fn remove(&mut self, key: &str) -> Option<PropertyDocument> {
237        self.inner.shift_remove(key)
238    }
239
240    pub fn remove_at(&mut self, idx: usize) -> Option<(String, PropertyDocument)> {
241        self.inner.shift_remove_index(idx)
242    }
243
244    pub fn replace_at(
245        &mut self,
246        idx: usize,
247        key: PropertyKey,
248        value: PropertyDocument,
249    ) -> Option<(String, PropertyDocument)> {
250        let entry = self.inner.get_index_entry(idx);
251        if let Some(mut entry) = entry {
252            use indexmap::map::MutableEntryKey;
253            let old_key = std::mem::replace(entry.key_mut(), key);
254            let old_value = std::mem::replace(entry.get_mut(), value);
255            return Some((old_key, old_value));
256        } else {
257            self.insert(key, value);
258        }
259        None
260    }
261
262    pub fn contains_key(&self, key: &str) -> bool {
263        self.inner.contains_key(key)
264    }
265
266    pub fn is_empty(&self) -> bool {
267        self.inner.is_empty()
268    }
269}
270
271impl From<ReadonlyProperties> for Properties {
272    fn from(parsed_properties: ReadonlyProperties) -> Self {
273        let mut properties = Properties::new();
274
275        for (prop_key, parsed_prop) in parsed_properties.into_iter() {
276            let editable_prop = EditableDocument::from(parsed_prop);
277            properties.inner.insert(prop_key, editable_prop);
278        }
279
280        properties
281    }
282}
283
284impl Display for Properties {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        for (property_key, property_doc) in &self.inner {
287            // Print property's documentation content
288            for doc_line in &property_doc.doc_texts {
289                writeln!(f, "{}", doc_line)?;
290            }
291
292            // Print property
293            if let Some(value) = &property_doc.value {
294                writeln!(f, "{}={}", property_key, value)?;
295            } else {
296                writeln!(f, "{}", property_key)?;
297            }
298        }
299        Ok(())
300    }
301}
302
303#[derive(Error, Debug)]
304pub enum ParseError {
305    #[error("parse error: {0}")]
306    FailedParse(String),
307}
308
309#[derive(Error, Debug)]
310pub enum ConfigError {
311    #[error("section {section:?} not found")]
312    SectionNotFound { section: Option<String> },
313    #[error("property '{property}' not found in section {section:?}")]
314    PropertyNotFound {
315        section: Option<String>,
316        property: String,
317    },
318}
319
320#[derive(Error, Debug)]
321pub enum IniError {
322    #[error(transparent)]
323    Parse(#[from] ParseError),
324    #[error(transparent)]
325    Config(#[from] ConfigError),
326}
327
328pub struct ReadonlyIni {
329    sections: Map<SectionKey, ParsedDocument<ReadonlyProperties>>,
330}
331
332impl ReadonlyIni {
333    pub fn section(&self, name: Option<&str>) -> Option<&ParsedDocument<ReadonlyProperties>> {
334        let key = name.map(|s| s.to_string());
335        self.sections.get(&key)
336    }
337
338    pub fn sections(&self) -> &Map<SectionKey, ParsedDocument<ReadonlyProperties>> {
339        &self.sections
340    }
341
342    pub fn get_property(
343        &self,
344        section_name: Option<&str>,
345        key: &str,
346    ) -> Option<&ParsedPropertyDocument> {
347        self.section(section_name)
348            .and_then(|section| section.get(key))
349    }
350}
351
352impl From<ReadonlyIni> for Ini {
353    fn from(readonly_ini: ReadonlyIni) -> Self {
354        let editable_sections = readonly_ini
355            .sections
356            .into_iter()
357            .map(|(section_key, parsed_section)| {
358                let editable_properties = Properties::from(parsed_section.data);
359                let editable_section =
360                    EditableDocument::new(editable_properties, parsed_section.doc_texts);
361                (section_key, editable_section)
362            })
363            .collect();
364
365        Ini {
366            sections: editable_sections,
367        }
368    }
369}
370
371impl TryFrom<Vec<Item<'_>>> for ReadonlyIni {
372    type Error = IniError;
373
374    fn try_from(value: Vec<Item<'_>>) -> Result<Self, Self::Error> {
375        let mut sections = Map::new();
376        // Create initial structure for global section
377        sections.insert(
378            None,
379            ParsedDocument::new(ReadonlyProperties::new(), 0, vec![]),
380        );
381
382        let mut current_section: Option<String> = None;
383        let mut pending_docs: Vec<String> = Vec::new();
384        let mut current_line_num = 1; // Start counting from line 1
385
386        for item in value {
387            match item {
388                Item::Blank { raw } => {
389                    pending_docs.push(raw.to_string());
390                    current_line_num += 1; // Count blank lines too
391                }
392                Item::Comment { raw, .. } => {
393                    pending_docs.push(raw.to_string());
394                    current_line_num += 1; // Count comment lines too
395                }
396                Item::Section { name, raw: _ } => {
397                    // Save current pending_docs for new section
398                    let section_docs = if !pending_docs.is_empty() {
399                        pending_docs.drain(..).collect()
400                    } else {
401                        vec![]
402                    };
403
404                    // Create new section, record current line number
405                    let new_section = ParsedDocument::new(
406                        ReadonlyProperties::new(),
407                        current_line_num,
408                        section_docs,
409                    );
410                    sections.insert(Some(name.to_string()), new_section);
411                    current_section = Some(name.to_string());
412
413                    current_line_num += 1; // Count section lines
414                }
415                Item::Property { key, val, raw: _ } => {
416                    let section_key = current_section.clone();
417
418                    // Create PropertyValue and PropertyDocument with line number
419                    let property_value = PropertyValue {
420                        value: val.map(|v| v.to_string()),
421                    };
422
423                    let docs = if !pending_docs.is_empty() {
424                        pending_docs.drain(..).collect()
425                    } else {
426                        vec![]
427                    };
428
429                    let property_doc = ParsedDocument::new(property_value, current_line_num, docs);
430
431                    // Insert property to corresponding section
432                    if let Some(section_doc) = sections.get_mut(&section_key) {
433                        section_doc.data.inner.insert(key.to_string(), property_doc);
434                    }
435
436                    current_line_num += 1; // Count property lines
437                }
438                Item::SectionEnd => {
439                    // SectionEnd does not need special handling
440                }
441                Item::Error(err) => {
442                    return Err(ParseError::FailedParse(err.to_string()).into());
443                }
444            }
445        }
446
447        Ok(ReadonlyIni { sections })
448    }
449}
450
451pub struct Ini {
452    sections: Map<SectionKey, SectionDocument>,
453}
454
455impl Ini {
456    /// Create a new INI instance, preset a general section (None)
457    pub fn new() -> Self {
458        let mut sections = Map::new();
459        // Preset a general section
460        sections.insert(None, Properties::new().into());
461        Self { sections }
462    }
463
464    /// Ensure a named section exists in the INI document.
465    ///
466    /// If the section does not exist, it will be created. The general section (None) cannot be created explicitly.
467    ///
468    /// # Arguments
469    /// * `section_name` - The name of the section to ensure exists.
470    pub fn set_section(&mut self, section_name: &str) {
471        let section_key = Some(section_name.to_string());
472        self.sections
473            .entry(section_key)
474            .or_insert_with(|| Properties::new().into());
475    }
476
477    /// Set a property value in a section.
478    ///
479    /// The section must already exist. If the property does not exist, it will be created; if it exists, it will be overwritten.
480    /// If `value` is `Some`, sets the key-value pair; if `None`, sets only the key name (no value).
481    ///
482    /// # Arguments
483    /// * `section_name` - The section name (None for general section).
484    /// * `key` - The property key.
485    /// * `value` - The property value (optional).
486    ///
487    /// # Errors
488    /// Returns `ConfigError::SectionNotFound` if the section does not exist.
489    pub fn set_property<T: ToString>(
490        &mut self,
491        section_name: Option<&str>,
492        key: &str,
493        value: Option<T>,
494    ) -> Result<(), ConfigError> {
495        let section_key = section_name.map(|s| s.to_string());
496
497        // Get section, return error if not exists
498        let properties =
499            self.sections
500                .get_mut(&section_key)
501                .ok_or_else(|| ConfigError::SectionNotFound {
502                    section: section_name.map(|s| s.to_string()),
503                })?;
504
505        // Set property
506        let property_value = PropertyValue {
507            value: value.map(|v| v.to_string()),
508        };
509        properties.set(key.to_string(), property_value);
510
511        Ok(())
512    }
513
514    /// Set documentation comments for a section.
515    ///
516    /// # Arguments
517    /// * `section_name` - The section name (None for general section).
518    /// * `doc_texts` - Documentation lines to associate with the section.
519    ///
520    /// # Errors
521    /// Returns `ConfigError::SectionNotFound` if the section does not exist.
522    pub fn set_section_doc(
523        &mut self,
524        section_name: Option<&str>,
525        doc_texts: Vec<String>,
526    ) -> Result<(), ConfigError> {
527        let section_key = section_name.map(|s| s.to_string());
528
529        // Get section, error if not exists
530        let section_doc =
531            self.sections
532                .get_mut(&section_key)
533                .ok_or_else(|| ConfigError::SectionNotFound {
534                    section: section_name.map(|s| s.to_string()),
535                })?;
536
537        // Update documentation
538        section_doc.doc_texts = doc_texts;
539        Ok(())
540    }
541
542    /// Set documentation comments for a property.
543    ///
544    /// # Arguments
545    /// * `section_name` - The section name (None for general section).
546    /// * `key` - The property key.
547    /// * `doc_texts` - Documentation lines to associate with the property.
548    ///
549    /// # Errors
550    /// Returns `ConfigError::SectionNotFound` if the section does not exist, or `ConfigError::PropertyNotFound` if the property does not exist.
551    pub fn set_property_doc(
552        &mut self,
553        section_name: Option<&str>,
554        key: &str,
555        doc_texts: Vec<String>,
556    ) -> Result<(), ConfigError> {
557        let section_key = section_name.map(|s| s.to_string());
558
559        // Get properties in the section, error if not exists
560        let properties =
561            self.sections
562                .get_mut(&section_key)
563                .ok_or_else(|| ConfigError::SectionNotFound {
564                    section: section_name.map(|s| s.to_string()),
565                })?;
566
567        // Get property
568        let property = properties
569            .get_mut(key)
570            .ok_or(ConfigError::PropertyNotFound {
571                section: section_name.map(|s| s.to_string()),
572                property: key.to_string(),
573            })?;
574
575        property.set_doc_texts(doc_texts);
576        Ok(())
577    }
578
579    /// Get the value of the specified section and key, parse to the specified type via FromStr
580    pub fn get_value<T: FromStr>(
581        &self,
582        section_name: Option<&str>,
583        key: &str,
584    ) -> Result<Option<T>, T::Err> {
585        let section_key = section_name.map(|s| s.to_string());
586
587        if let Some(properties) = self.sections.get(&section_key) {
588            return properties.get_value(key);
589        }
590        Ok(None)
591    }
592
593    /// Get the raw string value of the specified section and key
594    pub fn get_string(&self, section_name: Option<&str>, key: &str) -> Option<&str> {
595        let section_key = section_name.map(|s| s.to_string());
596
597        if let Some(section_doc) = self.sections.get(&section_key) {
598            if let Some(property_doc) = section_doc.data.get(key) {
599                return property_doc.value.as_deref();
600            }
601        }
602        None
603    }
604
605    /// Check if the specified property exists
606    pub fn has_property(&self, section_name: Option<&str>, key: &str) -> bool {
607        let section_key = section_name.map(|s| s.to_string());
608
609        if let Some(section_doc) = self.sections.get(&section_key) {
610            return section_doc.data.contains_key(key);
611        }
612        false
613    }
614
615    /// Remove the specified property
616    pub fn remove_property(&mut self, section_name: Option<&str>, key: &str) -> bool {
617        let section_key = section_name.map(|s| s.to_string());
618        match self.sections.get_mut(&section_key) {
619            Some(properties) => properties.remove(key).is_some(),
620            None => false,
621        }
622    }
623
624    /// Remove the specified section
625    pub fn remove_section(&mut self, section_name: Option<&str>) -> bool {
626        let section_key = section_name.map(|s| s.to_string());
627        self.sections.shift_remove(&section_key).is_some()
628    }
629
630    /// Get all sections
631    pub fn sections(&self) -> &Map<SectionKey, SectionDocument> {
632        &self.sections
633    }
634
635    /// Get the specified section properties
636    pub fn section(&self, name: Option<&str>) -> Option<&SectionDocument> {
637        let key = name.map(|s| s.to_string());
638        self.sections.get(&key)
639    }
640
641    /// Get a mutable reference to the specified section properties
642    pub fn section_mut(&mut self, name: Option<&str>) -> Option<&mut SectionDocument> {
643        let key = name.map(|s| s.to_string());
644        self.sections.get_mut(&key)
645    }
646}
647
648impl Display for Ini {
649    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650        // First handle the global section (no name)
651        // std::collections::HashMap orders are not guaranteed, so we handle the global section first
652        if let Some(global_section) = self.sections.get(&None) {
653            // Print documentation content of the global section
654            for doc_line in &global_section.doc_texts {
655                writeln!(f, "{}", doc_line)?;
656            }
657            // Print properties of the global section
658            write!(f, "{}", global_section.data)?;
659        }
660
661        // Then handle named sections
662        for (section_key, section_doc) in &self.sections {
663            if section_key.is_none() {
664                continue; // Global section already handled
665            }
666
667            // Print documentation content of the section
668            for doc_line in &section_doc.doc_texts {
669                writeln!(f, "{}", doc_line)?;
670            }
671
672            // Print section header
673            if let Some(section_name) = section_key {
674                writeln!(f, "[{}]", section_name)?;
675            }
676
677            // Print properties of the section
678            write!(f, "{}", section_doc.data)?;
679        }
680        Ok(())
681    }
682}
683
684impl FromStr for Ini {
685    type Err = IniError;
686
687    fn from_str(s: &str) -> Result<Self, Self::Err> {
688        let items: Vec<Item> = ini_engine::Parser::new(s).collect();
689        Self::try_from(items)
690    }
691}
692
693impl TryFrom<Vec<Item<'_>>> for Ini {
694    type Error = IniError;
695
696    fn try_from(value: Vec<Item<'_>>) -> Result<Self, Self::Error> {
697        // First parse to ParsedIni, then use From trait to convert to Ini
698        let parsed_ini: ReadonlyIni = value.try_into()?;
699        Ok(Ini::from(parsed_ini))
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn test_item_structure() {
709        // For debugging the structure of Item
710        let content = r#"
711; This is a comment
712[section1]
713; Property comment
714key1=value1
715
716[section2]
717key2=value2
718"#;
719
720        // Let's see how ini_roundtrip parses
721        let items = ini_engine::Parser::new(content).collect::<Vec<_>>();
722        for item in items {
723            println!("{:?}", item);
724        }
725    }
726
727    #[test]
728    fn test_parse_and_display() {
729        let content = r#"
730; Global comment
731global_key=global_value
732
733; section1 comment
734[section1]
735; key1 comment
736key1=value1
737
738; key2 comment
739key2=value2
740
741[section2]
742key3=value3
743"#;
744
745        let ini: Ini = content.parse().expect("Parse failed");
746        let result = ini.to_string();
747        println!("Parse result:\n{}", result);
748    }
749
750    #[test]
751    fn test_round_trip() {
752        let content = r#"; Config file header comment
753
754; Database config
755[database]
756; Host address
757host=localhost
758; Port number  
759port=3306
760
761user=admin
762
763; Web service config
764[web]
765; Listen port
766listen_port=8080
767; Static directory
768static_dir=/var/www
769"#;
770
771        let ini: Ini = content.parse().expect("Parse failed");
772        let result = ini.to_string();
773
774        // Verify that parsing the output again does not fail
775        let _ini2: Ini = result.parse().expect("second parse failed");
776
777        println!("Original content:\n{}", content);
778        println!("Parsed result:\n{}", result);
779
780        // Verify expected sections and properties
781        assert!(
782            ini.section(Some("database")).is_some(),
783            "should contain database section"
784        );
785        assert!(
786            ini.section(Some("web")).is_some(),
787            "should contain web section"
788        );
789
790        let db_section = ini.section(Some("database")).unwrap();
791        assert!(
792            db_section.contains_key("host"),
793            "database section should contain host"
794        );
795        assert!(
796            db_section.contains_key("port"),
797            "database section should contain port"
798        );
799        assert!(
800            db_section.contains_key("user"),
801            "database section should contain user"
802        );
803    }
804
805    #[test]
806    fn test_document_display() {
807        // Test the Display implementation of Document<T>
808        let property_value = PropertyValue {
809            value: Some("test_value".to_string()),
810        };
811
812        let doc_texts = vec![
813            "; This is a comment".to_string(),
814            String::new(),
815            "; Another comment".to_string(),
816        ];
817
818        let property_doc = Document::new(property_value, doc_texts);
819        let result = property_doc.to_string();
820
821        println!("PropertyDocument Display result:\n{}", result);
822        assert!(result.contains("; This is a comment"));
823        assert!(result.contains("; Another comment"));
824        assert!(result.contains("test_value"));
825
826        // Test the Display of Properties
827        let mut properties = Properties::new();
828        properties.insert("key1".to_string(), property_doc);
829
830        let properties_result = properties.to_string();
831        println!("Properties Display result:\n{}", properties_result);
832        assert!(properties_result.contains("key1=test_value"));
833
834        // Test the Display of SectionDocument
835        let section_docs = vec!["; Section comment".to_string()];
836        let section_doc = Document::new(properties, section_docs);
837
838        let section_result = section_doc.to_string();
839        println!("SectionDocument Display result:\n{}", section_result);
840        assert!(section_result.contains("; Section comment"));
841        assert!(section_result.contains("key1=test_value"));
842    }
843
844    #[test]
845    fn test_original_content_preservation() {
846        // Test original content preservation
847        let content = r#"; This is an original comment, with special format
848#This is a hash comment
849global_key=global_value
850
851; section1 comment, with spaces   
852[section1]
853;no space comment
854key1=value1
855    ; indented comment
856key2=value2
857"#;
858
859        let ini: Ini = content.parse().expect("Parse failed");
860        let result = ini.to_string();
861
862        println!("Original content:\n{}", content);
863        println!("Reconstructed result:\n{}", result);
864
865        // Verify original comment format is preserved
866        assert!(result.contains("; This is an original comment, with special format"));
867        assert!(result.contains("#This is a hash comment"));
868        assert!(result.contains("; section1 comment, with spaces   "));
869        assert!(result.contains(";no space comment"));
870        assert!(result.contains("; indented comment")); // Note: indentation may not be preserved
871
872        // Verify property values are correct
873        assert!(result.contains("global_key=global_value"));
874        assert!(result.contains("key1=value1"));
875        assert!(result.contains("key2=value2"));
876    }
877
878    #[test]
879    fn test_ini_editing() {
880        let mut ini = Ini::new();
881
882        // Test setting section and properties
883        ini.set_section("database");
884        ini.set_section("flags");
885
886        ini.set_property(Some("database"), "host", Some("localhost"))
887            .unwrap();
888        ini.set_property(Some("database"), "port", Some(3306))
889            .unwrap();
890        ini.set_property(None, "global_key", Some("global_value"))
891            .unwrap();
892        ini.set_property(Some("flags"), "debug", None::<String>)
893            .unwrap(); // Only key name, no value
894
895        // Test getting values
896        assert_eq!(ini.get_string(Some("database"), "host"), Some("localhost"));
897        assert_eq!(
898            ini.get_value::<i32>(Some("database"), "port").unwrap(),
899            Some(3306)
900        );
901        assert_eq!(ini.get_string(None, "global_key"), Some("global_value"));
902        assert_eq!(ini.get_string(Some("flags"), "debug"), None); // Only key name, no value
903
904        // Test if property exists
905        assert!(ini.has_property(Some("database"), "host"));
906        assert!(ini.has_property(Some("flags"), "debug"));
907        assert!(!ini.has_property(Some("database"), "nonexistent"));
908
909        // Test overwriting property
910        ini.set_property(Some("database"), "host", Some("127.0.0.1"))
911            .unwrap();
912        assert_eq!(ini.get_string(Some("database"), "host"), Some("127.0.0.1"));
913
914        // Test setting documentation
915        ini.set_section_doc(
916            Some("database"),
917            vec![
918                "; Database configuration".to_string(),
919                "; Important configuration".to_string(),
920            ],
921        )
922        .unwrap();
923        ini.set_property_doc(
924            Some("database"),
925            "host",
926            vec!["; Database host address".to_string()],
927        )
928        .unwrap();
929
930        println!("Edited INI:\n{}", ini);
931
932        // Verify documentation settings
933        let result = ini.to_string();
934        assert!(result.contains("; Database configuration"));
935        assert!(result.contains("; Important configuration"));
936        assert!(result.contains("; Database host address"));
937    }
938
939    #[test]
940    fn test_ini_deletion() {
941        let mut ini = Ini::new();
942
943        // Add some data
944        ini.set_section("section1");
945        ini.set_section("section2");
946
947        ini.set_property(Some("section1"), "key1", Some("value1"))
948            .unwrap();
949        ini.set_property(Some("section1"), "key2", Some("value2"))
950            .unwrap();
951        ini.set_property(Some("section2"), "key3", Some("value3"))
952            .unwrap();
953
954        // Test removing properties
955        assert!(ini.remove_property(Some("section1"), "key1"));
956        assert!(!ini.has_property(Some("section1"), "key1"));
957        assert!(ini.has_property(Some("section1"), "key2"));
958
959        // Test removing non-existent property
960        assert!(!ini.remove_property(Some("section1"), "nonexistent"));
961
962        // Test removing section
963        assert!(ini.remove_section(Some("section2")));
964        assert!(!ini.has_property(Some("section2"), "key3"));
965
966        // Test removing non-existent section
967        assert!(!ini.remove_section(Some("nonexistent")));
968    }
969
970    #[test]
971    fn test_type_conversion() {
972        let mut ini = Ini::new();
973
974        // Set different types of values
975        ini.set_section("config");
976
977        ini.set_property(Some("config"), "port", Some(8080))
978            .unwrap();
979        ini.set_property(Some("config"), "timeout", Some(30.5))
980            .unwrap();
981        ini.set_property(Some("config"), "enabled", Some(true))
982            .unwrap();
983        ini.set_property(Some("config"), "name", Some("test_server"))
984            .unwrap();
985
986        // Test type conversion
987        assert_eq!(
988            ini.get_value::<i32>(Some("config"), "port").unwrap(),
989            Some(8080)
990        );
991        assert_eq!(
992            ini.get_value::<f64>(Some("config"), "timeout").unwrap(),
993            Some(30.5)
994        );
995        assert_eq!(
996            ini.get_value::<bool>(Some("config"), "enabled").unwrap(),
997            Some(true)
998        );
999        assert_eq!(
1000            ini.get_value::<String>(Some("config"), "name").unwrap(),
1001            Some("test_server".to_string())
1002        );
1003
1004        // Test type conversion failure
1005        assert!(ini.get_value::<i32>(Some("config"), "name").is_err());
1006
1007        // Test non-existent value
1008        assert_eq!(
1009            ini.get_value::<i32>(Some("config"), "nonexistent").unwrap(),
1010            None
1011        );
1012    }
1013
1014    #[test]
1015    fn test_edit_existing_ini() {
1016        // (Removed duplicated INI content block)
1017        let content = r#"; Original configuration
1018[database]
1019host=old_host
1020port=3306
1021
1022[web]
1023port=8080
1024"#;
1025
1026        let mut ini: Ini = content.parse().expect("Parse failed");
1027
1028        // Modify existing configuration
1029        ini.set_property(Some("database"), "host", Some("new_host"))
1030            .unwrap();
1031        ini.set_section("cache");
1032        ini.set_property(Some("database"), "user", Some("admin"))
1033            .unwrap(); // Add new property
1034        ini.set_property(Some("cache"), "enabled", Some(true))
1035            .unwrap(); // Add new section
1036
1037        // Modify documentation
1038        ini.set_property_doc(
1039            Some("database"),
1040            "host",
1041            vec![String::from("; New host address")],
1042        )
1043        .unwrap();
1044
1045        let result = ini.to_string();
1046        println!("Modified configuration:\n{}", result);
1047
1048        // Verify modification results
1049        assert_eq!(ini.get_string(Some("database"), "host"), Some("new_host"));
1050        assert_eq!(ini.get_string(Some("database"), "user"), Some("admin"));
1051        assert_eq!(
1052            ini.get_value::<bool>(Some("cache"), "enabled").unwrap(),
1053            Some(true)
1054        );
1055        assert!(result.contains("; New host address"));
1056    }
1057
1058    #[test]
1059    fn test_doc_validation() {
1060        let mut ini = Ini::new();
1061
1062        // Test that setting documentation for a nonexistent section should fail
1063        let result = ini.set_section_doc(
1064            Some("nonexistent"),
1065            vec![String::from("; Nonexistent section")],
1066        );
1067        assert!(result.is_err());
1068        match result.unwrap_err() {
1069            ConfigError::SectionNotFound { section } => {
1070                assert_eq!(section, Some("nonexistent".to_string()));
1071            }
1072            _ => panic!("Expected SectionNotFound error"),
1073        }
1074
1075        // Test that setting documentation for a nonexistent property should fail
1076        ini.set_section("test");
1077        ini.set_property(Some("test"), "key1", Some("value1"))
1078            .unwrap();
1079        let result = ini.set_property_doc(
1080            Some("test"),
1081            "nonexistent",
1082            vec![String::from("; Nonexistent property")],
1083        );
1084        assert!(result.is_err());
1085        match result.unwrap_err() {
1086            ConfigError::PropertyNotFound { section, property } => {
1087                assert_eq!(section, Some("test".to_string()));
1088                assert_eq!(property, "nonexistent");
1089            }
1090            _ => panic!("Expected PropertyNotFound error"),
1091        }
1092
1093        // Test that setting documentation for a property in a nonexistent section should fail
1094        let result = ini.set_property_doc(
1095            Some("nonexistent"),
1096            "key1",
1097            vec![String::from("; Nonexistent section")],
1098        );
1099        assert!(result.is_err());
1100        match result.unwrap_err() {
1101            ConfigError::SectionNotFound { section } => {
1102                assert_eq!(section, Some("nonexistent".to_string()));
1103            }
1104            _ => panic!("Expected SectionNotFound error"),
1105        }
1106
1107        // Test correct case
1108        assert!(
1109            ini.set_section_doc(Some("test"), vec![String::from("; Test section")])
1110                .is_ok()
1111        );
1112        assert!(
1113            ini.set_property_doc(Some("test"), "key1", vec![String::from("; Test property")])
1114                .is_ok()
1115        );
1116
1117        let result = ini.to_string();
1118        assert!(result.contains("; Test section"));
1119        assert!(result.contains("; Test property"));
1120    }
1121
1122    #[test]
1123    fn test_strict_section_behavior() {
1124        let mut ini = Ini::new();
1125
1126        // Test that setting a property in a nonexistent section should fail
1127        let result = ini.set_property(Some("nonexistent"), "key", Some("value"));
1128        assert!(result.is_err());
1129        match result.unwrap_err() {
1130            ConfigError::SectionNotFound { section } => {
1131                assert_eq!(section, Some("nonexistent".to_string()));
1132            }
1133            _ => panic!("Expected SectionNotFound error"),
1134        }
1135
1136        // Test that setting a property in the global section (None) should now succeed, as the global section exists by default
1137        let result = ini.set_property(None, "global_key", Some("value"));
1138        assert!(
1139            result.is_ok(),
1140            "Setting global property should succeed because the global section exists by default"
1141        );
1142
1143        // Correct workflow: create section first, then set property
1144        ini.set_section("test");
1145
1146        assert!(
1147            ini.set_property(Some("test"), "key1", Some("value1"))
1148                .is_ok()
1149        );
1150        assert!(
1151            ini.set_property(None, "another_global_key", Some("another_global_value"))
1152                .is_ok()
1153        );
1154
1155        // Verify values are set correctly
1156        assert_eq!(ini.get_string(Some("test"), "key1"), Some("value1"));
1157        assert_eq!(ini.get_string(None, "global_key"), Some("value")); // Previously set
1158        assert_eq!(
1159            ini.get_string(None, "another_global_key"),
1160            Some("another_global_value")
1161        );
1162
1163        // Test idempotency of set_section (calling it multiple times for the same section should not fail)
1164        ini.set_section("test"); // Set the same section again
1165        assert!(
1166            ini.set_property(Some("test"), "key2", Some("value2"))
1167                .is_ok()
1168        );
1169    }
1170
1171    #[test]
1172    fn test_debug_parsing() {
1173        let content = r#"; Global comment 1
1174global_key=global_value
1175
1176; section1 comment
1177[section1]
1178; key1 comment
1179key1=value1
1180"#;
1181
1182        println!("=== Debug parsing items ===");
1183        let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1184        for (i, item) in items.iter().enumerate() {
1185            println!("Item {}: {:?}", i, item);
1186        }
1187    }
1188
1189    #[test]
1190    fn test_parsed_ini_line_number_tracking() {
1191        let content = r#"; Global comment 1
1192global_key=global_value
1193
1194; section1 comment
1195[section1]
1196; key1 comment
1197key1=value1
1198
1199; key2 comment  
1200key2=value2
1201
1202[section2]
1203key3=value3
1204"#;
1205
1206        // First parse to ParsedIni to get line number information
1207        let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1208        let parsed_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1209
1210        // Verify global section line number
1211        let global_section = parsed_ini.section(None).unwrap();
1212        println!("Global section line_num: {}", global_section.line_num());
1213        assert_eq!(global_section.line_num(), 0); // Default to 0
1214
1215        // Verify global_key line number
1216        assert_eq!(
1217            parsed_ini
1218                .get_property(None, "global_key")
1219                .map(|v| v.line_num()),
1220            Some(2)
1221        );
1222
1223        // Verify section1 line number
1224        assert_eq!(
1225            parsed_ini.section(Some("section1")).map(|s| s.line_num()),
1226            Some(5)
1227        );
1228
1229        // Verify property line numbers
1230        assert_eq!(
1231            parsed_ini
1232                .get_property(Some("section1"), "key1")
1233                .map(|v| v.line_num()),
1234            Some(7)
1235        );
1236        assert_eq!(
1237            parsed_ini
1238                .get_property(Some("section1"), "key2")
1239                .map(|v| v.line_num()),
1240            Some(10)
1241        );
1242
1243        // Verify section2 line number
1244        assert_eq!(
1245            parsed_ini.section(Some("section2")).map(|s| s.line_num()),
1246            Some(12)
1247        );
1248        assert_eq!(
1249            parsed_ini
1250                .get_property(Some("section2"), "key3")
1251                .map(|v| v.line_num()),
1252            Some(13)
1253        );
1254
1255        // Verify document content is correctly preserved
1256        let global_property = parsed_ini
1257            .section(None)
1258            .unwrap()
1259            .data
1260            .inner
1261            .get("global_key")
1262            .unwrap();
1263        assert_eq!(
1264            global_property.doc_texts(),
1265            &["; Global comment 1".to_string()]
1266        );
1267
1268        let section1 = parsed_ini.section(Some("section1")).unwrap();
1269        assert_eq!(
1270            section1.doc_texts(),
1271            &["", "; section1 comment"].map(|s| s.to_string())
1272        );
1273
1274        let key1_property = section1.data.inner.get("key1").unwrap();
1275        assert_eq!(key1_property.doc_texts(), &["; key1 comment".to_string()]);
1276
1277        // Test converting to editable structure
1278        let editable_ini: Ini = parsed_ini.into();
1279        assert_eq!(
1280            editable_ini.get_string(None, "global_key"),
1281            Some("global_value")
1282        );
1283        assert_eq!(
1284            editable_ini.get_string(Some("section1"), "key1"),
1285            Some("value1")
1286        );
1287    }
1288
1289    #[test]
1290    fn test_editable_ini_functionality() {
1291        let content = r#"; Global comment 1
1292global_key=global_value
1293
1294; section1 comment
1295[section1]
1296; key1 comment
1297key1=value1
1298
1299; key2 comment  
1300key2=value2
1301
1302[section2]
1303key3=value3
1304"#;
1305
1306        let ini: Ini = content.parse().expect("Parse failed");
1307
1308        // Test basic functionality of editable structure
1309        assert_eq!(ini.get_string(None, "global_key"), Some("global_value"));
1310        assert_eq!(ini.get_string(Some("section1"), "key1"), Some("value1"));
1311        assert_eq!(ini.get_string(Some("section1"), "key2"), Some("value2"));
1312        assert_eq!(ini.get_string(Some("section2"), "key3"), Some("value3"));
1313
1314        // Verify document content is correctly preserved (but without line number information)
1315        let global_section = ini.section(None).unwrap();
1316        let global_property = global_section.data.get("global_key").unwrap();
1317        assert_eq!(
1318            global_property.doc_texts(),
1319            &["; Global comment 1".to_string()]
1320        );
1321
1322        let section1 = ini.section(Some("section1")).unwrap();
1323        // Note: Due to blank line processing, this may contain empty strings
1324        assert!(
1325            section1
1326                .doc_texts()
1327                .contains(&"; section1 comment".to_string())
1328        );
1329
1330        let key1_property = section1.data.get("key1").unwrap();
1331        assert_eq!(key1_property.doc_texts(), &["; key1 comment".to_string()]);
1332
1333        let key2_property = section1.data.get("key2").unwrap();
1334        // Check that document content contains expected comments, ignoring blank lines and spaces
1335        assert!(
1336            key2_property
1337                .doc_texts()
1338                .iter()
1339                .any(|line| line.trim() == "; key2 comment")
1340        );
1341    }
1342
1343    #[test]
1344    fn test_architecture_separation() {
1345        let content = r#"; Config header
1346global_setting=value
1347
1348[database]
1349host=localhost
1350port=3306
1351"#;
1352
1353        // 1. Parse to ParsedIni (preserve line number information)
1354        let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1355        let parsed_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1356
1357        // Check line number information
1358        assert_eq!(
1359            parsed_ini
1360                .get_property(None, "global_setting")
1361                .map(|v| v.line_num()),
1362            Some(2)
1363        );
1364        assert_eq!(
1365            parsed_ini.section(Some("database")).map(|s| s.line_num()),
1366            Some(4)
1367        );
1368        assert_eq!(
1369            parsed_ini
1370                .get_property(Some("database"), "host")
1371                .map(|v| v.line_num()),
1372            Some(5)
1373        );
1374        assert_eq!(
1375            parsed_ini
1376                .get_property(Some("database"), "port")
1377                .map(|v| v.line_num()),
1378            Some(6)
1379        );
1380
1381        // 2. 转换为可编辑的 Ini(丢弃行号,保留内容)
1382        let mut editable_ini: Ini = parsed_ini.into();
1383
1384        // Verify content conversion is correct
1385        assert_eq!(
1386            editable_ini.get_string(None, "global_setting"),
1387            Some("value")
1388        );
1389        assert_eq!(
1390            editable_ini.get_string(Some("database"), "host"),
1391            Some("localhost")
1392        );
1393        assert_eq!(
1394            editable_ini.get_string(Some("database"), "port"),
1395            Some("3306")
1396        );
1397
1398        // 3. 在可编辑结构上进行修改
1399        editable_ini
1400            .set_property(Some("database"), "host", Some("127.0.0.1"))
1401            .unwrap();
1402        editable_ini.set_section("cache");
1403        editable_ini
1404            .set_property(Some("cache"), "enabled", Some(true))
1405            .unwrap();
1406
1407        // 4. Verify modified content
1408        assert_eq!(
1409            editable_ini.get_string(Some("database"), "host"),
1410            Some("127.0.0.1")
1411        );
1412        assert_eq!(
1413            editable_ini
1414                .get_value::<bool>(Some("cache"), "enabled")
1415                .unwrap(),
1416            Some(true)
1417        );
1418
1419        println!(
1420            "Architecture separation test successful: parsing preserves line numbers, editing focuses on content"
1421        );
1422    }
1423
1424    #[test]
1425    fn test_from_parsed_ini_trait() {
1426        let content = r#"; Config header
1427global_setting=value
1428
1429[database]
1430host=localhost
1431port=3306
1432"#;
1433
1434        // 1. 解析为 ParsedIni
1435        let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1436        let parsed_ini: ReadonlyIni = items.try_into().expect("解析失败");
1437
1438        // 2. 使用 From trait 转换为 Ini
1439        let editable_ini: Ini = parsed_ini.into();
1440
1441        // 3. Verify conversion result
1442        assert_eq!(
1443            editable_ini.get_string(None, "global_setting"),
1444            Some("value")
1445        );
1446        assert_eq!(
1447            editable_ini.get_string(Some("database"), "host"),
1448            Some("localhost")
1449        );
1450        assert_eq!(
1451            editable_ini.get_string(Some("database"), "port"),
1452            Some("3306")
1453        );
1454
1455        println!("From trait conversion test successful");
1456    }
1457
1458    #[test]
1459    fn test_from_parsed_document_trait() {
1460        // 创建一个 ParsedDocument
1461        let property_value = PropertyValue {
1462            value: Some("test_value".to_string()),
1463        };
1464        let doc_texts = vec!["; Test comment".to_string()];
1465        let parsed_doc = ParsedDocument::new(property_value, 5, doc_texts.clone());
1466
1467        // 使用 From trait 转换为 EditableDocument
1468        let editable_doc: EditableDocument<PropertyValue> = parsed_doc.into();
1469
1470        // Verify conversion result
1471        assert_eq!(editable_doc.doc_texts(), &doc_texts);
1472        assert_eq!(editable_doc.value, Some("test_value".to_string()));
1473
1474        println!("ParsedDocument From trait conversion test successful");
1475    }
1476
1477    #[test]
1478    fn test_all_from_traits() {
1479        // Test all From trait implementations
1480
1481        // 1. PropertyValue -> EditableDocument<PropertyValue>
1482        let property_value = PropertyValue {
1483            value: Some("test_value".to_string()),
1484        };
1485        let editable_prop: EditableDocument<PropertyValue> = property_value.into();
1486        assert_eq!(editable_prop.value, Some("test_value".to_string()));
1487
1488        // 2. Properties -> EditableDocument<Properties>
1489        let properties = Properties::new();
1490        let editable_section: EditableDocument<Properties> = properties.into();
1491        assert!(editable_section.is_empty());
1492
1493        // 3. ParsedDocument<PropertyValue> -> EditableDocument<PropertyValue>
1494        let parsed_prop = ParsedDocument::new(
1495            PropertyValue {
1496                value: Some("parsed_value".to_string()),
1497            },
1498            10,
1499            vec!["; Comment".to_string()],
1500        );
1501        let editable_prop2: EditableDocument<PropertyValue> = parsed_prop.into();
1502        assert_eq!(editable_prop2.value, Some("parsed_value".to_string()));
1503        assert_eq!(editable_prop2.doc_texts(), &["; Comment".to_string()]);
1504
1505        // 4. ParsedProperties -> Properties (Test through ParsedIni since ParsedProperties internals are private)
1506
1507        // 5. ParsedIni -> Ini (Test through parsing)
1508        let content = r#"key=value"#;
1509        let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1510        let parsed_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1511        let editable_ini: Ini = parsed_ini.into();
1512        assert_eq!(editable_ini.get_string(None, "key"), Some("value"));
1513
1514        println!("All From trait conversion tests successful");
1515    }
1516}