Created
April 3, 2023 14:47
-
-
Save chanmix51/0cfdd4b4a28e41b739d459688ba6ef6c to your computer and use it in GitHub Desktop.
FlatConfig example & TODO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::{collections::HashMap, error::Error, fmt::Display, path::PathBuf}; | |
#[derive(Debug)] | |
pub enum ConfigError { | |
/// Configuration setting named is missing. | |
Missing(String), | |
/// Type mismatch | |
TypeMismatch { expected: String, present: String }, | |
/// The value is incorrect, give a useful context error message (field name, why the value was | |
/// wrong or what was expected. | |
IncorrectValue(String), | |
} | |
impl Display for ConfigError { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
match self { | |
Self::Missing(field) => write!(f, "CONFIGURATION ERROR: Field '{field}' is missing."), | |
Self::TypeMismatch { expected, present } => { | |
write!( | |
f, | |
"CONFIGURATION ERROR: Type mismatch, expected '{expected}' got '{present}'." | |
) | |
} | |
Self::IncorrectValue(message) => { | |
write!(f, "CONFIGURATION ERROR: Incorrect value: {message}.") | |
} | |
} | |
} | |
} | |
impl Error for ConfigError {} | |
#[derive(Debug, Clone)] | |
pub enum ConfigSetting { | |
Integer(isize), | |
Text(String), | |
Boolean(bool), | |
} | |
impl ConfigSetting { | |
fn display(&self) -> String { | |
let subtype: &str = match self { | |
Self::Integer(_) => "integer", | |
Self::Text(_) => "text", | |
Self::Boolean(_) => "boolean", | |
}; | |
subtype.to_string() | |
} | |
fn try_unwrap_integer(&self) -> Result<isize, ConfigError> { | |
match self { | |
Self::Integer(i) => Ok(*i), | |
_ => Err(ConfigError::TypeMismatch { | |
expected: "integer".to_string(), | |
present: self.display(), | |
}), | |
} | |
} | |
fn try_unwrap_text(&self) -> Result<String, ConfigError> { | |
match self { | |
Self::Text(t) => Ok(t.to_string()), | |
_ => Err(ConfigError::TypeMismatch { | |
expected: "text".to_string(), | |
present: self.display(), | |
}), | |
} | |
} | |
fn try_unwrap_bool(&self) -> Result<bool, ConfigError> { | |
match self { | |
Self::Boolean(b) => Ok(*b), | |
_ => Err(ConfigError::TypeMismatch { | |
expected: "boolean".to_string(), | |
present: self.display(), | |
}), | |
} | |
} | |
} | |
impl From<isize> for ConfigSetting { | |
fn from(value: isize) -> Self { | |
Self::Integer(value) | |
} | |
} | |
impl From<&str> for ConfigSetting { | |
fn from(value: &str) -> Self { | |
Self::Text(value.to_string()) | |
} | |
} | |
impl From<bool> for ConfigSetting { | |
fn from(value: bool) -> Self { | |
Self::Boolean(value) | |
} | |
} | |
#[derive(Debug, Default)] | |
pub struct ConfigSettingPool { | |
settings: HashMap<String, ConfigSetting>, | |
} | |
impl ConfigSettingPool { | |
pub fn add(&mut self, name: &str, value: ConfigSetting) -> &mut Self { | |
self.settings.insert(name.to_string(), value); | |
self | |
} | |
pub fn get(&self, name: &str) -> Option<&ConfigSetting> { | |
self.settings.get(name) | |
} | |
pub fn has(&self, name: &str) -> bool { | |
self.settings.contains_key(name) | |
} | |
} | |
pub trait ConfigBuilder<T> { | |
fn build(&self, config_pool: &ConfigSettingPool) -> Result<T, ConfigError>; | |
fn get( | |
&self, | |
config_pool: &ConfigSettingPool, | |
name: &str, | |
) -> Result<ConfigSetting, ConfigError> { | |
config_pool | |
.get(name) | |
.cloned() | |
.ok_or_else(|| ConfigError::Missing(name.to_string())) | |
} | |
fn get_or( | |
&self, | |
config_pool: &ConfigSettingPool, | |
name: &str, | |
default: ConfigSetting, | |
) -> ConfigSetting { | |
self.get(config_pool, name).unwrap_or(default) | |
} | |
} | |
#[derive(Debug, Default, Clone)] | |
struct AppConfiguration { | |
environment: String, | |
database_dir: PathBuf, | |
verbose_level: usize, | |
dry_run: bool, | |
} | |
#[derive(Default)] | |
struct AppConfigBuilder; | |
impl ConfigBuilder<AppConfiguration> for AppConfigBuilder { | |
fn build(&self, config_pool: &ConfigSettingPool) -> Result<AppConfiguration, ConfigError> { | |
let environment = self.get(config_pool, "environment")?.try_unwrap_text()?; | |
let database_dir = self.get(config_pool, "database_dir")?.try_unwrap_text()?; | |
let database_dir = PathBuf::new().join(database_dir); | |
let verbose_level = self | |
.get_or(config_pool, "verbose_level", 0.into()) | |
.try_unwrap_integer()?; | |
let verbose_level = usize::try_from(verbose_level).map_err(|e| { | |
ConfigError::IncorrectValue(format!( | |
"Verbose level ({verbose_level}) could notbe converted to usize. Error: {e}" | |
)) | |
})?; | |
let dry_run = self | |
.get_or(config_pool, "dry_run", false.into()) | |
.try_unwrap_bool()?; | |
let config = AppConfiguration { | |
environment, | |
database_dir: PathBuf::new().join(database_dir), | |
verbose_level, | |
dry_run, | |
}; | |
Ok(config) | |
} | |
} | |
fn main() -> Result<(), String> { | |
let mut pool = ConfigSettingPool::default(); | |
pool.add("environment", "production".into()) | |
//.add("database_dir", "/var/database".into()) | |
.add("verbose_level", 2.into()) | |
.add("dry_run", true.into()); | |
let config = AppConfigBuilder::default() | |
.build(&pool) | |
.map_err(|e| format!("{e}"))?; | |
println!("{config:?}"); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment