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#[derive(Clone)]
17pub struct ReadonlyDocument<'a, T> {
18 doc_texts: Vec<&'a str>,
20 line_num: usize,
21 data: T,
22}
23
24#[derive(Clone)]
27pub struct EditableDocument<T> {
28 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
51pub 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 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
167impl 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 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 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 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 let section_docs = if !pending_docs.is_empty() {
436 pending_docs.drain(..).collect()
437 } else {
438 vec![]
439 };
440
441 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 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 if let Some(section_doc) = sections.get_mut(§ion_key) {
463 section_doc.data.inner.insert(key, property_doc);
464 }
465 }
466 Item::SectionEnd => {
467 }
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 pub fn new() -> Self {
486 let mut sections = Map::new();
487 sections.insert(None, SectionDocument::new(Properties::new(), vec![]));
489 Self { sections }
490 }
491
492 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 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 let properties =
527 self.sections
528 .get_mut(§ion_key)
529 .ok_or_else(|| ConfigError::SectionNotFound {
530 section: section_name.map(|s| s.to_string()),
531 })?;
532
533 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 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 let section_doc =
559 self.sections
560 .get_mut(§ion_key)
561 .ok_or_else(|| ConfigError::SectionNotFound {
562 section: section_name.map(|s| s.to_string()),
563 })?;
564
565 section_doc.doc_texts = doc_texts;
567 Ok(())
568 }
569
570 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 let properties =
589 self.sections
590 .get_mut(§ion_key)
591 .ok_or_else(|| ConfigError::SectionNotFound {
592 section: section_name.map(|s| s.to_string()),
593 })?;
594
595 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 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(§ion_key) {
616 return properties.get_value(key);
617 }
618 Ok(None)
619 }
620
621 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(§ion_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 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(§ion_key) {
638 return section_doc.data.contains_key(key);
639 }
640 false
641 }
642
643 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(§ion_key) {
647 Some(properties) => properties.remove(key).is_some(),
648 None => false,
649 }
650 }
651
652 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(§ion_key).is_some()
656 }
657
658 pub fn sections(&self) -> &Map<Option<String>, SectionDocument> {
660 &self.sections
661 }
662
663 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 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 if let Some(global_section) = self.sections.get(&None) {
681 for doc_line in &global_section.doc_texts {
683 writeln!(f, "{}", doc_line)?;
684 }
685 write!(f, "{}", global_section.data)?;
687 }
688
689 for (section_key, section_doc) in &self.sections {
691 if section_key.is_none() {
692 continue;
694 }
695
696 for doc_line in §ion_doc.doc_texts {
698 writeln!(f, "{}", doc_line)?;
699 }
700
701 if let Some(section_name) = section_key {
703 writeln!(f, "[{}]", section_name)?;
704 }
705
706 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 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 let content = r#"
739; This is a comment
740[section1]
741; Property comment
742key1=value1
743
744[section2]
745key2=value2
746"#;
747
748 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 let _ini2: Ini = result.parse().expect("second parse failed");
804
805 println!("Original content:\n{}", content);
806 println!("Readonly result:\n{}", result);
807
808 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 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 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 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 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 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")); 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 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(); 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); 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 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 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 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 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 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 assert!(!ini.remove_property(Some("section1"), "nonexistent"));
985
986 assert!(ini.remove_section(Some("section2")));
988 assert!(!ini.has_property(Some("section2"), "key3"));
989
990 assert!(!ini.remove_section(Some("nonexistent")));
992 }
993
994 #[test]
995 fn test_type_conversion() {
996 let mut ini = Ini::new();
997
998 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 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 assert!(ini.get_value::<i32>(Some("config"), "name").is_err());
1030
1031 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 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 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(); ini.set_property(Some("cache"), "enabled", Some(true))
1059 .unwrap(); 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 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 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 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 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 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 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 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 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 assert_eq!(ini.get_string(Some("test"), "key1"), Some("value1"));
1181 assert_eq!(ini.get_string(None, "global_key"), Some("value")); assert_eq!(
1183 ini.get_string(None, "another_global_key"),
1184 Some("another_global_value")
1185 );
1186
1187 ini.set_section("test"); 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1232 let readonly_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1233
1234 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); assert_eq!(
1241 readonly_ini
1242 .get_property(None, "global_key")
1243 .map(|v| v.line_num()),
1244 Some(2)
1245 );
1246
1247 assert_eq!(
1249 readonly_ini.section(Some("section1")).map(|s| s.line_num()),
1250 Some(5)
1251 );
1252
1253 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 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 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 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 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 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 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 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1379 let readonly_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1380
1381 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 let mut editable_ini: Ini = readonly_ini.into();
1407
1408 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 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 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1460 let readonly_ini: ReadonlyIni = items.try_into().expect("解析失败");
1461
1462 let editable_ini: Ini = readonly_ini.into();
1464
1465 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 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 let editable_doc: EditableDocument<PropertyValue<String>> = readonly_doc.into();
1493
1494 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 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 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 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 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}