diff --git a/src/ini.rs b/src/ini.rs index 02127cc..4fb4110 100755 --- a/src/ini.rs +++ b/src/ini.rs @@ -339,6 +339,57 @@ impl Ini { Ok(self.map.clone()) } + ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. + ///While `load()` will clear the existing `Map`, `load_and_append()` applies the new values on top of + ///the existing hashmap, preserving previous values. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini").unwrap(); + ///config.load_and_append("tests/sys_cfg.ini").ok(); // we don't have to worry if this doesn't succeed + ///config.load_and_append("tests/user_cfg.ini").ok(); // we don't have to worry if this doesn't succeed + ///let map = config.get_map().unwrap(); + /////Then, we can use standard hashmap functions like: + ///let values = map.get("values").unwrap(); + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn load_and_append>( + &mut self, + path: T, + ) -> Result>>, String> { + let loaded = match self.parse(match fs::read_to_string(&path) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(s) => s, + }) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(map) => map, + }; + + for (section, section_map) in loaded.iter() { + self.map + .entry(section.clone()) + .or_insert_with(Map::new) + .extend(section_map.clone()); + } + + Ok(self.map.clone()) + } + ///Reads an input string, parses it and puts the hashmap into our struct. ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. ///## Example @@ -368,6 +419,52 @@ impl Ini { Ok(self.map.clone()) } + ///Reads an input string, parses it and applies it to the existing hashmap in our struct. + ///While `read()` and `load()` will clear the existing `Map`, `read_and_append()` applies the new + ///values on top of the existing hashmap, preserving previous values. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///if let Err(why) = config.read(String::from( + /// "[2000s] + /// 2020 = bad + /// 2023 = better")) { + /// panic!("{}", why); + ///}; + ///if let Err(why) = config.read_and_append(String::from( + /// "[2000s] + /// 2020 = terrible")) { + /// panic!("{}", why); + ///}; + ///let map = config.get_map().unwrap(); + ///let few_years_ago = map["2000s"]["2020"].clone().unwrap(); + ///let this_year = map["2000s"]["2023"].clone().unwrap(); + ///assert_eq!(few_years_ago, "terrible"); // value updated! + ///assert_eq!(this_year, "better"); // keeps old values! + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn read_and_append( + &mut self, + input: String, + ) -> Result>>, String> { + let loaded = match self.parse(input) { + Err(why) => return Err(why), + Ok(map) => map, + }; + + for (section, section_map) in loaded.iter() { + self.map + .entry(section.clone()) + .or_insert_with(Map::new) + .extend(section_map.clone()); + } + + Ok(self.map.clone()) + } + ///Writes the current configuation to the specified path. If a file is not present, it is automatically created for you, if a file already ///exists, it is truncated and the configuration is written to it. ///## Example @@ -963,6 +1060,48 @@ impl Ini { Ok(self.map.clone()) } + ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. + ///While `load_async()` will clear the existing `Map`, `load_and_append_async()` applies the new values on top + ///of the existing hashmap, preserving previous values. + /// + ///Usage is similar to `load_and_append`, but `.await` must be called after along with the usual async rules. + /// + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub async fn load_and_append_async>( + &mut self, + path: T, + ) -> Result>>, String> { + let loaded = match self.parse(match async_fs::read_to_string(&path).await { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(s) => s, + }) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(map) => map, + }; + + for (section, section_map) in loaded.iter() { + self.map + .entry(section.clone()) + .or_insert_with(Map::new) + .extend(section_map.clone()); + } + + Ok(self.map.clone()) + } + ///Writes the current configuation to the specified path asynchronously. If a file is not present, it is automatically created for you, if a file already ///exists, it is truncated and the configuration is written to it. /// diff --git a/tests/test.rs b/tests/test.rs index b0f473c..e2744e6 100755 --- a/tests/test.rs +++ b/tests/test.rs @@ -100,6 +100,32 @@ fn non_cs() -> Result<(), Box> { mut_map.clear(); config2.clear(); assert_eq!(config.get_map_ref(), config2.get_map_ref()); + + config.load("tests/test.ini")?; + config.read_and_append("defaultvalues=somenewvalue".to_owned())?; + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "somenewvalue" + ); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + + let mut config3 = config.clone(); + let mut_map = config3.get_mut_map(); + mut_map.clear(); + config3.load("tests/test.ini")?; + config3.load_and_append("tests/test_more.ini")?; + assert_eq!( + config3.get("default", "defaultvalues").unwrap(), + "overwritten" + ); + assert_eq!(config3.get("topsecret", "KFC").unwrap(), "redacted"); + // spacing -> indented exists in tests/test.ini, but not tests/test_more.ini + assert_eq!(config3.get("spacing", "indented").unwrap(), "indented"); + assert_eq!(config3.getbool("values", "Bool")?.unwrap(), false); + Ok(()) } @@ -284,6 +310,27 @@ fn async_load_write() -> Result<(), Box> { Ok(()) } +#[test] +#[cfg(feature = "async-std")] +fn async_load_and_append() -> Result<(), Box> { + let mut sync_content = Ini::new(); + sync_content.load("tests/test.ini")?; + sync_content.load_and_append("tests/test_more.ini")?; + + let async_content = async_std::task::block_on::<_, Result<_, String>>(async { + let mut async_content = Ini::new(); + async_content.load_async("tests/test.ini").await?; + async_content + .load_and_append_async("tests/test_more.ini") + .await?; + Ok(async_content) + })?; + + assert_eq!(sync_content, async_content); + + Ok(()) +} + #[test] #[cfg(feature = "indexmap")] fn multiline_off() -> Result<(), Box> { diff --git a/tests/test_more.ini b/tests/test_more.ini new file mode 100755 index 0000000..629eec8 --- /dev/null +++ b/tests/test_more.ini @@ -0,0 +1,8 @@ +defaultvalues=overwritten + +[topsecret] +KFC = redacted + +[values] +Bool = False +