diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100755 new mode 100644 diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 3d2d073..6b2c152 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ If you read the above sections carefully, you'll know that 1) all the keys are s manner and 3) we can use `getuint()` to parse the `Uint` value into an `u64`. Let's see that in action. ```rust -use configparser::ini::Ini; +use configparser::ini::{Ini, WriteOptions}; use std::error::Error; fn main() -> Result<(), Box> { @@ -142,9 +142,16 @@ fn main() -> Result<(), Box> { let innermap = map["topsecret"].clone(); // Remember that all indexes are stored in lowercase! - // You can easily write the currently stored configuration to a file like: + // You can easily write the currently stored configuration to a file with the `write` method. This creates a compact format with as little spacing as possible: config.write("output.ini"); + // You can write the currently stored configuration with more spacing to a file with the `pretty_write` method. You must supply the method with a configuratio specification: + let mut write_options = WriteOptions::default(); // The defaults match the formatting used in the `write` method + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + config.pretty_write("pretty_output.ini", &write_options); + // If you want to simply mutate the stored hashmap, you can use get_mut_map() let map = config.get_mut_map(); // You can then use normal HashMap functions on this map at your convenience. diff --git a/src/ini.rs b/src/ini.rs old mode 100755 new mode 100644 index 4fb4110..faf07f3 --- a/src/ini.rs +++ b/src/ini.rs @@ -130,6 +130,56 @@ impl Default for IniDefault { } } +/// Use this struct to define formatting options for the `pretty_write` functions. +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct WriteOptions { + ///If true then the keys and values will be separated by " = ". In the special case where the value is empty, the + ///line ends with " =". + ///If false then keys and values will be separated by "=". + ///Default is `false`. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.space_around_delimiters, false); + ///``` + pub space_around_delimiters: bool, + + ///Defines the number of spaces for indentation of for multiline values. + ///Default is 4 spaces. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.multiline_line_indentation, 4); + ///``` + pub multiline_line_indentation: usize, + + ///Defines the number of blank lines between sections. + ///Default is 0. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.blank_lines_between_sections, 0); + ///``` + pub blank_lines_between_sections: usize, +} + +impl Default for WriteOptions { + fn default() -> Self { + Self { + space_around_delimiters: false, + multiline_line_indentation: 4, + blank_lines_between_sections: 0, + } + } +} + #[cfg(windows)] const LINE_ENDING: &str = "\r\n"; #[cfg(not(windows))] @@ -465,8 +515,8 @@ impl Ini { 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. + ///Writes the current configuation to the specified path using default formatting. + ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. ///## Example ///```rust ///use configparser::ini::Ini; @@ -481,11 +531,39 @@ impl Ini { ///``` ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. pub fn write>(&self, path: T) -> std::io::Result<()> { - fs::write(path.as_ref(), self.unparse()) + fs::write(path.as_ref(), self.unparse(&WriteOptions::default())) + } + + ///Writes the current configuation to the specified path using the given formatting options. + ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. + ///## Example + ///```rust + ///use configparser::ini::{Ini, WriteOptions}; + /// + ///fn main() -> std::io::Result<()> { + /// let mut write_options = WriteOptions::default(); + /// write_options.space_around_delimiters = true; + /// write_options.multiline_line_indentation = 2; + /// write_options.blank_lines_between_sections = 1; + /// + /// let mut config = Ini::new(); + /// config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + /// config.pretty_write("output.ini", &write_options) + ///} + ///``` + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub fn pretty_write>( + &self, + path: T, + write_options: &WriteOptions, + ) -> std::io::Result<()> { + fs::write(path.as_ref(), self.unparse(write_options)) } - ///Returns a string with the current configuration formatted with valid ini-syntax. This is always safe since the configuration is validated during - ///parsing. + ///Returns a string with the current configuration formatted with valid ini-syntax using default formatting. + ///This is always safe since the configuration is validated during parsing. ///## Example ///```rust ///use configparser::ini::Ini; @@ -498,22 +576,51 @@ impl Ini { ///``` ///Returns a `String` type contatining the ini-syntax file. pub fn writes(&self) -> String { - self.unparse() + self.unparse(&WriteOptions::default()) + } + + ///Returns a string with the current configuration formatted with valid ini-syntax using the given formatting options. + ///This is always safe since the configuration is validated during parsing. + ///## Example + ///```rust + ///use configparser::ini::{Ini, WriteOptions}; + /// + ///let mut write_options = WriteOptions::default(); + ///write_options.space_around_delimiters = true; + ///write_options.multiline_line_indentation = 2; + ///write_options.blank_lines_between_sections = 1; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + ///let outstring = config.pretty_writes(&write_options); + ///``` + ///Returns a `String` type contatining the ini-syntax file. + pub fn pretty_writes(&self, write_options: &WriteOptions) -> String { + self.unparse(write_options) } ///Private function that converts the currently stored configuration into a valid ini-syntax string. - fn unparse(&self) -> String { + fn unparse(&self, write_options: &WriteOptions) -> String { // push key/value pairs in outmap to out string. fn unparse_key_values( out: &mut String, outmap: &Map>, multiline: bool, + space_around_delimiters: bool, + indent: usize, ) { + let delimiter = if space_around_delimiters { " = " } else { "=" }; for (key, val) in outmap.iter() { out.push_str(key); if let Some(value) = val { - out.push('='); + if value.is_empty() { + out.push_str(delimiter.trim_end()); + } else { + out.push_str(delimiter); + } if multiline { let mut lines = value.lines(); @@ -522,7 +629,7 @@ impl Ini { for line in lines { out.push_str(LINE_ENDING); - out.push_str(" "); + out.push_str(" ".repeat(indent).as_ref()); out.push_str(line); } } else { @@ -534,18 +641,36 @@ impl Ini { } } + let line_endings = LINE_ENDING.repeat(write_options.blank_lines_between_sections); let mut out = String::new(); if let Some(defaultmap) = self.map.get(&self.default_section) { - unparse_key_values(&mut out, defaultmap, self.multiline); + unparse_key_values( + &mut out, + defaultmap, + self.multiline, + write_options.space_around_delimiters, + write_options.multiline_line_indentation, + ); } + let mut is_first = true; for (section, secmap) in self.map.iter() { + if !is_first { + out.push_str(line_endings.as_ref()); + } if section != &self.default_section { write!(out, "[{}]", section).unwrap(); out.push_str(LINE_ENDING); - unparse_key_values(&mut out, secmap, self.multiline); + unparse_key_values( + &mut out, + secmap, + self.multiline, + write_options.space_around_delimiters, + write_options.multiline_line_indentation, + ); } + is_first = false; } out } @@ -1102,13 +1227,27 @@ impl Ini { 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 + ///Writes the current configuation to the specified path asynchronously using default formatting. 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. /// ///Usage is the same as `write`, but `.await` must be called after along with the usual async rules. /// ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. pub async fn write_async>(&self, path: T) -> std::io::Result<()> { - async_fs::write(path.as_ref(), self.unparse()).await + async_fs::write(path.as_ref(), self.unparse(&WriteOptions::default())).await + } + + ///Writes the current configuation to the specified path asynchronously using the given formatting options. 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. + /// + ///Usage is the same as `pretty_pretty_write`, but `.await` must be called after along with the usual async rules. + /// + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub async fn pretty_write_async>( + &self, + path: T, + write_options: &WriteOptions, + ) -> std::io::Result<()> { + async_fs::write(path.as_ref(), self.unparse(write_options)).await } } diff --git a/src/lib.rs b/src/lib.rs old mode 100755 new mode 100644 index b313a56..6305f13 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ If you read the above sections carefully, you'll know that 1) all the keys are s manner and 3) we can use `getint()` to parse the `Int` value into an `i64`. Let's see that in action. ```rust -use configparser::ini::Ini; +use configparser::ini::{Ini, WriteOptions}; use std::error::Error; fn main() -> Result<(), Box> { @@ -134,9 +134,16 @@ fn main() -> Result<(), Box> { let innermap = map["topsecret"].clone(); // Remember that all indexes are stored in lowercase! - // You can easily write the currently stored configuration to a file like: + // You can easily write the currently stored configuration to a file with the `write` method. This creates a compact format with as little spacing as possible: config.write("output.ini"); + // You can write the currently stored configuration with more spacing to a file with the `pretty_write` method. You must supply the method with a configuratio specification: + let mut write_options = WriteOptions::default(); // The defaults match the formatting used in the `write` method + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + config.pretty_write("pretty_output.ini", &write_options); + // If you want to simply mutate the stored hashmap, you can use get_mut_map() let map = config.get_mut_map(); // You can then use normal HashMap functions on this map at your convenience. diff --git a/tests/test.ini b/tests/test.ini old mode 100755 new mode 100644 diff --git a/tests/test.rs b/tests/test.rs old mode 100755 new mode 100644 index e2744e6..4313aa1 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,4 +1,4 @@ -use configparser::ini::Ini; +use configparser::ini::{Ini, WriteOptions}; use std::error::Error; #[test] @@ -261,6 +261,173 @@ Float=3.1415 Ok(()) } +#[test] +#[cfg(feature = "indexmap")] +fn pretty_writes_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + assert_eq!( + config.pretty_writes(&write_options), + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +#[cfg(feature = "async-std")] +fn pretty_write_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + config.pretty_write("pretty_output.ini", &write_options)?; + + let file_contents = std::fs::read_to_string("pretty_output.ini")?; + assert_eq!( + file_contents, + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +#[cfg(feature = "async-std")] +fn async_pretty_print_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + async_std::task::block_on::<_, Result<_, String>>(async { + config + .pretty_write_async("pretty_output_async.ini", &write_options) + .await + .map_err(|e| e.to_string())?; + Ok(()) + })?; + + let file_contents = std::fs::read_to_string("pretty_output_async.ini")?; + assert_eq!( + file_contents, + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + #[test] #[cfg(feature = "async-std")] fn async_load_write() -> Result<(), Box> { diff --git a/tests/test_more.ini b/tests/test_more.ini old mode 100755 new mode 100644