Skip to content

Instantly share code, notes, and snippets.

@chanmix51
Created April 3, 2023 14:47
Show Gist options
  • Save chanmix51/0cfdd4b4a28e41b739d459688ba6ef6c to your computer and use it in GitHub Desktop.
Save chanmix51/0cfdd4b4a28e41b739d459688ba6ef6c to your computer and use it in GitHub Desktop.
FlatConfig example & TODO
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