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