pub use config_derive::ConfigOption; use thiserror::Error; #[derive(Debug, Error)] pub enum ConfigReadError { #[error("Cannot read the configuration file: {0}")] CannotRead(std::io::Error), #[error("Cannot open the configuration file for reading: {0}")] CannotOpen(std::io::Error), #[error("Invalid json data found in the configurationfile: {0}")] InvalidJSON(serde_json::Error), } macro_rules! define_config { ($($name:ident($struct:ident),)+) => ( use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs::File, hash::Hash, io::{ErrorKind, Read}, path::PathBuf, }; #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum ConfigName { $($name),+ } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ConfigOption { $($name($struct)),+ } #[derive(Clone, Debug)] pub struct Config { values: HashMap, } impl Config { pub fn new() -> Self { Self { values: HashMap::new(), } } pub fn from_path(config_path: PathBuf) -> Result { let mut settings = config_path.clone(); settings.push("config"); match File::open(settings) { Ok(mut file) => { let mut buf = String::new(); file.read_to_string(&mut buf) .map_err(|err| ConfigReadError::CannotRead(err))?; let values = serde_json::from_str(buf.as_ref()) .map_err(|err| ConfigReadError::InvalidJSON(err))?; Ok(Self { values, }) } Err(io_err) => { match io_err.kind() { ErrorKind::NotFound => { /* create the path and an empty file */ Ok(Self { values: HashMap::new(), }) } _ => Err(ConfigReadError::CannotOpen(io_err)), } } } } pub fn set(&mut self, val: ConfigOption) { let _ = match val { $(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+ }; } pub fn get<'a, T>(&'a self) -> Option where Option: From<&'a Self>, { self.into() } } ) } #[cfg(test)] mod test { use super::*; use cool_asserts::assert_matches; define_config! { DatabasePath(DatabasePath), Me(Me), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)] pub struct DatabasePath(PathBuf); #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] enum Rank { Kyu(i8), Dan(i8), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)] pub struct Me { name: String, rank: Option, } #[test] fn it_can_set_and_get_options() { let mut config: Config = Config::new(); config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( "./fixtures/five_games", )))); assert_eq!( Some(DatabasePath(PathBuf::from("./fixtures/five_games"))), config.get() ); } #[test] fn it_can_serialize_and_deserialize() { let mut config = Config::new(); config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( "fixtures/five_games", )))); config.set(ConfigOption::Me(Me { name: "Savanni".to_owned(), rank: Some(Rank::Kyu(10)), })); let s = serde_json::to_string(&config.values).unwrap(); println!("{}", s); let values: HashMap = serde_json::from_str(s.as_ref()).unwrap(); println!("options: {:?}", values); assert_matches!(values.get(&ConfigName::DatabasePath), Some(ConfigOption::DatabasePath(ref db_path)) => assert_eq!(Some(db_path.clone()), config.get()) ); assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) => assert_eq!(Some(val.clone()), config.get()) ); } }