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 ParsedDocument<T> {
18 doc_texts: Vec<String>,
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
33pub 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 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 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 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 for doc_line in &property_doc.doc_texts {
289 writeln!(f, "{}", doc_line)?;
290 }
291
292 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 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; for item in value {
387 match item {
388 Item::Blank { raw } => {
389 pending_docs.push(raw.to_string());
390 current_line_num += 1; }
392 Item::Comment { raw, .. } => {
393 pending_docs.push(raw.to_string());
394 current_line_num += 1; }
396 Item::Section { name, raw: _ } => {
397 let section_docs = if !pending_docs.is_empty() {
399 pending_docs.drain(..).collect()
400 } else {
401 vec![]
402 };
403
404 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; }
415 Item::Property { key, val, raw: _ } => {
416 let section_key = current_section.clone();
417
418 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 if let Some(section_doc) = sections.get_mut(§ion_key) {
433 section_doc.data.inner.insert(key.to_string(), property_doc);
434 }
435
436 current_line_num += 1; }
438 Item::SectionEnd => {
439 }
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 pub fn new() -> Self {
458 let mut sections = Map::new();
459 sections.insert(None, Properties::new().into());
461 Self { sections }
462 }
463
464 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 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 let properties =
499 self.sections
500 .get_mut(§ion_key)
501 .ok_or_else(|| ConfigError::SectionNotFound {
502 section: section_name.map(|s| s.to_string()),
503 })?;
504
505 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 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 let section_doc =
531 self.sections
532 .get_mut(§ion_key)
533 .ok_or_else(|| ConfigError::SectionNotFound {
534 section: section_name.map(|s| s.to_string()),
535 })?;
536
537 section_doc.doc_texts = doc_texts;
539 Ok(())
540 }
541
542 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 let properties =
561 self.sections
562 .get_mut(§ion_key)
563 .ok_or_else(|| ConfigError::SectionNotFound {
564 section: section_name.map(|s| s.to_string()),
565 })?;
566
567 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 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(§ion_key) {
588 return properties.get_value(key);
589 }
590 Ok(None)
591 }
592
593 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(§ion_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 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(§ion_key) {
610 return section_doc.data.contains_key(key);
611 }
612 false
613 }
614
615 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(§ion_key) {
619 Some(properties) => properties.remove(key).is_some(),
620 None => false,
621 }
622 }
623
624 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(§ion_key).is_some()
628 }
629
630 pub fn sections(&self) -> &Map<SectionKey, SectionDocument> {
632 &self.sections
633 }
634
635 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 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 if let Some(global_section) = self.sections.get(&None) {
653 for doc_line in &global_section.doc_texts {
655 writeln!(f, "{}", doc_line)?;
656 }
657 write!(f, "{}", global_section.data)?;
659 }
660
661 for (section_key, section_doc) in &self.sections {
663 if section_key.is_none() {
664 continue; }
666
667 for doc_line in §ion_doc.doc_texts {
669 writeln!(f, "{}", doc_line)?;
670 }
671
672 if let Some(section_name) = section_key {
674 writeln!(f, "[{}]", section_name)?;
675 }
676
677 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 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 let content = r#"
711; This is a comment
712[section1]
713; Property comment
714key1=value1
715
716[section2]
717key2=value2
718"#;
719
720 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 let _ini2: Ini = result.parse().expect("second parse failed");
776
777 println!("Original content:\n{}", content);
778 println!("Parsed result:\n{}", result);
779
780 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 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 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 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 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 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")); 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 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(); 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); 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 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 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 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 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 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 assert!(!ini.remove_property(Some("section1"), "nonexistent"));
961
962 assert!(ini.remove_section(Some("section2")));
964 assert!(!ini.has_property(Some("section2"), "key3"));
965
966 assert!(!ini.remove_section(Some("nonexistent")));
968 }
969
970 #[test]
971 fn test_type_conversion() {
972 let mut ini = Ini::new();
973
974 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 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 assert!(ini.get_value::<i32>(Some("config"), "name").is_err());
1006
1007 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 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 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(); ini.set_property(Some("cache"), "enabled", Some(true))
1035 .unwrap(); 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 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 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 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 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 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 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 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 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 assert_eq!(ini.get_string(Some("test"), "key1"), Some("value1"));
1157 assert_eq!(ini.get_string(None, "global_key"), Some("value")); assert_eq!(
1159 ini.get_string(None, "another_global_key"),
1160 Some("another_global_value")
1161 );
1162
1163 ini.set_section("test"); 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1208 let parsed_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1209
1210 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); assert_eq!(
1217 parsed_ini
1218 .get_property(None, "global_key")
1219 .map(|v| v.line_num()),
1220 Some(2)
1221 );
1222
1223 assert_eq!(
1225 parsed_ini.section(Some("section1")).map(|s| s.line_num()),
1226 Some(5)
1227 );
1228
1229 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 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 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 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 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 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 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 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1355 let parsed_ini: ReadonlyIni = items.try_into().expect("Parse failed");
1356
1357 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 let mut editable_ini: Ini = parsed_ini.into();
1383
1384 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 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 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 let items: Vec<Item> = ini_engine::Parser::new(content).collect();
1436 let parsed_ini: ReadonlyIni = items.try_into().expect("解析失败");
1437
1438 let editable_ini: Ini = parsed_ini.into();
1440
1441 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 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 let editable_doc: EditableDocument<PropertyValue> = parsed_doc.into();
1469
1470 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 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 let properties = Properties::new();
1490 let editable_section: EditableDocument<Properties> = properties.into();
1491 assert!(editable_section.is_empty());
1492
1493 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 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}