361 lines
11 KiB
Rust
361 lines
11 KiB
Rust
//! 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<String>),
|
|
}
|
|
|
|
struct ConfigSection {
|
|
header: String,
|
|
body: HashMap<String, Value>,
|
|
}
|
|
|
|
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<String>)`, internal conversion to string
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// `array` array of type, type has to implement `Display` trait
|
|
#[deprecated]
|
|
pub fn from_array<T: Display>(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<T: Display>(value: &T) -> Self {
|
|
Value::Value(format!("{}", value))
|
|
}
|
|
|
|
pub fn to_array<T: FromStr>(&self) -> Result<Vec<T>> {
|
|
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::<T>() {
|
|
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<T: FromStr>(&self) -> Result<T> {
|
|
match self {
|
|
Value::Value(value_string) => match value_string.parse::<T>() {
|
|
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<Path>,
|
|
) -> Result<HashMap<String, HashMap<String, Value>>> {
|
|
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<ConfigSection> = 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<Path>,
|
|
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<String, std::collections::HashMap<String, Value>>
|
|
) -> anyhow::Result<Self> {
|
|
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<assetpath::AssetPath>) -> anyhow::Result<Self> {
|
|
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<assetpath::AssetPath>) -> 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(),
|
|
)*
|
|
]
|
|
)
|
|
}
|
|
}
|
|
};
|
|
}
|