//! Config file handler //! Cares about formatting and type conversion use std::{ collections::HashMap, fmt::Display, fs::File, io::{BufRead, BufReader, Write}, path::Path, str::FromStr, }; use anyhow::{Context, Result}; /// Value abstraction to convert to and from values #[derive(Clone, Debug)] pub enum Value { Value(String), Array(Vec), } struct ConfigSection { header: String, body: HashMap, } impl Value { /// Creates an empty value pub fn empty() -> Value { Value::Value("".to_string()) } /// Creates an empty array value pub fn empty_array() -> Value { Value::Array(Vec::new()) } /// Create a value `Value::Array(Vec)`, internal conversion to string /// /// # Arguments /// /// `array` array of type, type has to implement `Display` trait #[deprecated] pub fn from_array(array: &[T]) -> Self { Value::Array(array.iter().map(|v| format!("{}", v)).collect()) } /// Creates a value `Value::Value(String)`, internal conversion to string /// /// # Arguments /// /// `value` type has to implement `Display` trait #[deprecated] pub fn from_value(value: &T) -> Self { Value::Value(format!("{}", value)) } pub fn to_array(&self) -> Result> { match self { Value::Array(value_array) => { let mut target_array = Vec::with_capacity(value_array.len()); for value_string in value_array { match value_string.parse::() { Ok(val) => target_array.push(val), Err(_) => { return Err(anyhow::Error::msg(format!( "ConfigHandler: Error while parsing Value Array: {}", value_string ))); } } } Ok(target_array) } _ => Err(anyhow::Error::msg( "ConfigHandler: Error when requesting the wrong value type", )), } } pub fn to_value(&self) -> Result { match self { Value::Value(value_string) => match value_string.parse::() { Ok(val) => Ok(val), Err(_) => Err(anyhow::Error::msg(format!( "ConfigHandler: Error while parsing Value Array: {}", value_string ))), }, _ => Err(anyhow::Error::msg( "ConfigHandler: Error when requesting the wrong value type", )), } } } impl<'a, T: Display> From<&'a T> for Value { fn from(v: &'a T) -> Self { Value::Value(format!("{}", v)) } } impl<'a, T: Display> From<&'a [T]> for Value { fn from(v: &'a [T]) -> Self { Value::Array(v.iter().map(|v| format!("{}", v)).collect()) } } /// Handler struct pub struct ConfigHandler {} impl ConfigHandler { /// Reads the given config file /// /// # Arguments /// /// `file_name` file that is going to be read pub fn read_config( file_name: impl AsRef, ) -> Result>> { let file = File::open(&file_name).with_context({ let file_name = file_name.as_ref().to_str().unwrap().to_string(); || file_name })?; let mut infos = HashMap::new(); let mut current_section: Option = None; for line_res in BufReader::new(file).lines() { if let Ok(line) = line_res { let mut trimmed = line.trim().to_string(); if trimmed.starts_with('#') || trimmed.is_empty() { continue; } else if trimmed.starts_with('[') && trimmed.ends_with(']') { trimmed.remove(0); trimmed.pop(); if let Some(ref section) = current_section { infos.insert(section.header.clone(), section.body.clone()); } current_section = Some(ConfigSection { header: trimmed, body: HashMap::new(), }); } else { let mut split = trimmed.split('='); let key = match split.nth(0) { Some(key) => key.trim().to_string(), None => { println!("cannot get key from line: {}", trimmed); continue; } }; let value = match split.last() { Some(value) => value.trim().to_string(), None => { println!("cannot get value from line: {}", trimmed); continue; } }; if value.starts_with('[') && value.ends_with(']') { let mut trimmed_value = value; trimmed_value.remove(0); trimmed_value.pop(); let value_split = trimmed_value.split(','); let mut value_array = Vec::new(); for v in value_split { let trimmed = v.trim(); if !trimmed.is_empty() { value_array.push(trimmed.to_string()); } } if let Some(ref mut section) = current_section { section.body.insert(key, Value::Array(value_array)); } } else if let Some(ref mut section) = current_section { section.body.insert(key, Value::Value(value)); } } } } // also push the last section if let Some(section) = current_section { infos.insert(section.header, section.body); } Ok(infos) } /// writes a formatted config file /// /// # Arguments /// /// `file_name` the file to which the config gets written /// `sections` the sections and keys that are going to be written pub fn write_config( file_name: impl AsRef, sections: &[(&str, Vec<(&str, Value)>)], ) -> Result<()> { let mut file = File::create(file_name)?; for (header, body) in sections { let fmt_header = format!("[{}]\n", header); file.write_all(fmt_header.as_bytes())?; for (key, value) in body.iter() { let fmt_key_value = format!( "{} = {}\n", key, match value { Value::Value(val) => val.clone(), Value::Array(array) => { let mut array_value = "[".to_string(); for (i, val) in array.iter().enumerate() { // if element is not the last one if i != array.len() - 1 { array_value = format!("{}{}, ", array_value, val); } else { array_value = format!("{}{}", array_value, val); } } format!("{}]", array_value) } } ); file.write_all(fmt_key_value.as_bytes())?; } file.write_all("\n".as_bytes())?; } Ok(()) } } #[macro_export] macro_rules! create_settings_section { ($struct_name:ident, $section_key:expr, {$($var:ident: $var_type:ty,)* $([$array:ident: $array_type:ty],)*} $(,$($derive:ident,)*)?) => { #[derive(Default, Debug, Clone, PartialEq $(, $($derive,)* )? )] pub struct $struct_name { $( pub $var: $var_type, )* $( pub $array: Vec<$array_type>, )* } impl $struct_name { const SECTION_KEY: &'static str = $section_key; pub fn load( parsed_config: &std::collections::HashMap> ) -> anyhow::Result { let mut me = Self::default(); if let Some(section) = parsed_config.get(Self::SECTION_KEY) { $( if let Some(value) = section.get(stringify!($var)) { me.$var = value.to_value()?; } )* $( if let Some(array) = section.get(stringify!($array)) { me.$array = array.to_array()?; } )* } Ok(me) } pub fn store(&self) -> (&str, Vec<(&str, Value)>) { ( Self::SECTION_KEY, vec![ $( (stringify!($var), Value::from(&self.$var)), )* $( (stringify!($array), Value::from(self.$array.as_slice())), )* ] ) } } }; } #[macro_export] macro_rules! create_settings_container { ($struct_name:ident, {$($var:ident: $var_type:ty$(,)?)*} $(,$($derive:ident,)*)? ) => { #[derive(Default, Debug, Clone, PartialEq $(, $($derive,)* )? )] pub struct $struct_name { pub file_name: assetpath::AssetPath, $( pub $var: $var_type, )* } impl $struct_name { pub fn load(file: impl Into) -> anyhow::Result { let file = file.into(); let parsed_stats_settings = ConfigHandler::read_config(&file.full_path())?; Ok($struct_name { file_name: file, $( $var: <$var_type>::load(&parsed_stats_settings)?, )* }) } pub fn load_with_default(&mut self, file: impl Into) -> anyhow::Result<()> { let file = file.into(); let parsed_stats_settings = ConfigHandler::read_config(&file.full_path())?; self.file_name = file; $( self.$var = <$var_type>::load(&parsed_stats_settings)?; )* Ok(()) } pub fn store(&self) -> anyhow::Result<()> { ConfigHandler::write_config( &self.file_name.full_path(), &[ $( self.$var.store(), )* ] ) } } }; }