use crate::types::Player; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs::File, io::{ErrorKind, Read}, path::PathBuf, }; use thiserror::Error; #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] enum OptionNames { Language, MusicPath, PlaylistDatabasePath, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum ConfigOption { Language(Language), MusicPath(MusicPath), PlaylistDatabasePath(PlaylistDatabasePath), } #[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), } #[derive(Clone, Debug)] pub struct Config { config_path: PathBuf, values: HashMap, } impl Config { pub fn new(config_path: PathBuf) -> Self { Self { config_path, 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 { config_path, values, }) } Err(io_err) => { match io_err.kind() { ErrorKind::NotFound => { /* create the path and an empty file */ Ok(Self { config_path, values: HashMap::new(), }) } _ => Err(ConfigReadError::CannotOpen(io_err)), } } } } pub fn set(&mut self, val: ConfigOption) { let _ = match val { ConfigOption::Language(_) => self.values.insert(OptionNames::Language, val), ConfigOption::MusicPath(_) => self.values.insert(OptionNames::MusicPath, val), ConfigOption::PlaylistDatabasePath(_) => { self.values.insert(OptionNames::PlaylistDatabasePath, val) } }; } pub fn get<'a, T>(&'a self) -> T where T: From<&'a Self>, { self.into() } } /* #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DatabasePath(PathBuf); impl std::ops::Deref for DatabasePath { type Target = PathBuf; fn deref(&self) -> &Self::Target { &self.0 } } impl From<&Config> for DatabasePath { fn from(config: &Config) -> Self { match config.values.get(&OptionNames::DatabasePath) { Some(ConfigOption::DatabasePath(path)) => path.clone(), _ => DatabasePath(config.config_path.clone()), } } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Me(Player); impl From<&Config> for Option { fn from(config: &Config) -> Self { config .values .get(&OptionNames::Me) .and_then(|val| match val { ConfigOption::Me(me) => Some(me.clone()), _ => None, }) } } impl std::ops::Deref for Me { type Target = Player; fn deref(&self) -> &Self::Target { &self.0 } } */ #[cfg(test)] mod test { use super::*; use crate::types::Rank; use cool_asserts::assert_matches; #[test] fn it_can_set_and_get_options() { let mut config = Config::new(PathBuf::from(".")); config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( "fixtures/five_games", )))); config.set(ConfigOption::Me(Me(Player { name: "Savanni".to_owned(), rank: Some(Rank::Kyu(10)), }))); } #[test] fn it_can_serialize_and_deserialize() { let mut config = Config::new(PathBuf::from(".")); config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( "fixtures/five_games", )))); config.set(ConfigOption::Me(Me(Player { 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(&OptionNames::DatabasePath), Some(ConfigOption::DatabasePath(db_path)) => assert_eq!(*db_path, config.get()) ); assert_matches!(values.get(&OptionNames::Me), Some(ConfigOption::Me(val)) => assert_eq!(Some(val.clone()), config.get()) ); } }