diff --git a/src/config.rs b/src/config.rs index 26bd889..7843212 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,13 +8,14 @@ use clap::Parser; use erased_debug::Erased; use serde::Deserialize; use std::{ + fmt::Debug, fs::read_to_string, io::Error as IoError, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, path::{Path, PathBuf}, }; use toml::{self, de::Error as TomlDeError}; -use tracing::info; +use tracing::{debug, info}; #[derive(Debug, Parser)] #[clap(about, author, version = crate::VERSION)] @@ -160,62 +161,36 @@ impl From for Error { } } -// this is a bit of a cluster fuck -// but this handles all the lookup bits in the right order +// this handles all the lookup bits in the right order // so it goes CLI -> env vars -> profile-specific overrides -> bare values in TOML -> hardcoded fallback macro_rules! config_field { ($first:ident.$second:ident, $field:ident, $type:ty$(, $default:expr)?) => { - pub fn $field(&self) -> Result<$type, Error> { + fn $field(&self) -> Result<$type, Error> { let value: Result<_, Error> = if let Some(ref $field) = self.cli.$field { + debug!($field = ?$field, "Read field from CLI (or env var)"); Ok($field.to_owned()) } else { - if let Some(profile) = self.profile_toml { - if let Some(ref outer) = profile.get(stringify!($first)) { - if let Some(inner) = outer.get(stringify!($second)) { - return Ok(<$type>::deserialize(inner.clone())?); - } - } - } - if let Some(ref outer) = self.default_toml.get(stringify!($first)) { - let value = outer.get(stringify!($second)).ok_or_else(|| { - Error::MissingField(concat!(stringify!($first), ".", stringify!($second))) - })?; - let as_type = <$type>::deserialize(value.clone())?; - Ok(as_type) - } else { - Err(Error::MissingField(stringify!($first)))? - } + let field = concat!(stringify!($first), ".", stringify!($second)); + self.toml_value_nested(stringify!($first), stringify!($second)).ok_or_else(|| Error::MissingField(field)) }; - value$(.or_else(|_| Ok($default)))? + value$(.or_else(|_| { debug!($field = ?$default, "Using default value {:?} for field", $default);Ok($default)}))? } }; ($field:ident, $type:ty$(, $default:expr)?) => { - pub fn $field(&self) -> Result<$type, Error> { + fn $field(&self) -> Result<$type, Error> { let value = if let Some(ref $field) = self.cli.$field { Ok::<_, Error>($field.to_owned()) } else { - if let Some(profile) = self.profile_toml { - if let Some($field) = profile.get(stringify!($field)) { - return Ok(<$type>::deserialize($field.clone())?); - } - } - - if let Some($field) = self.default_toml.get(stringify!($field)) { - let value = <$type>::deserialize($field.clone())?; - Ok(value) - } else { - Err(Error::MissingField(stringify!($field))) - } + self.toml_value(stringify!($field)) }; - value$(.or_else(|_| Ok($default)))? + value$(.or_else(|_| {debug!($field = ?$default, "Using default value {:?} for field", $default);Ok($default)}))? } }; } struct Merge<'a> { cli: Cli, - default_toml: &'a toml::Value, - profile_toml: Option<&'a toml::Value>, + toml: &'a toml::Value, } #[inline] @@ -226,11 +201,67 @@ fn is_docker() -> bool { } impl<'a> Merge<'a> { + #[cfg(debug_assertions)] + const PROFILE: &'static str = "debug"; + #[cfg(not(debug_assertions))] + const PROFILE: &'static str = "release"; + + fn profile_value>(&self, field: &'a str) -> Option { + let value = self + .toml + .get(Self::PROFILE) + .and_then(|v| v.get(field)) + .and_then(|v| T::deserialize(v.clone()).ok()); + if let Some(value) = &value { + debug!(field = field, value = ?value, "Read field from `{}` TOML table", Self::PROFILE); + } + value + } + + fn toml_value>(&self, field: &'static str) -> Result { + self.profile_value(field) + .or_else(|| { + let value = self.toml.get(field).and_then(|v| T::deserialize(v.clone()).ok()); + if let Some(value) = &value { + debug!(field = field, value = ?value, "Read field from top-level TOML table"); + } + value + }) + .ok_or_else(|| Error::MissingField(field)) + } + + fn profile_value_nested>(&self, outer: &'a str, inner: &'a str) -> Option { + let value = self + .toml + .get(Self::PROFILE) + .and_then(|v| v.get(outer)) + .and_then(|v| v.get(inner)) + .and_then(|v| T::deserialize(v.clone()).ok()); + if let Some(value) = &value { + debug!(outer = outer, inner = inner, value = ?value, "Read field from `{}` TOML table", Self::PROFILE); + } + value + } + + fn toml_value_nested>(&self, outer: &'static str, inner: &'a str) -> Option { + self.profile_value_nested(outer, inner).or_else(|| { + let value = self + .toml + .get(outer) + .and_then(|v| v.get(inner)) + .and_then(|v| T::deserialize(v.clone()).ok()); + if let Some(value) = &value { + debug!(outer = outer, inner = inner, value = ?value, "Read field from top-level TOML table"); + } + value + }) + } + config_field!(database.dsn, database_dsn, String, { if is_docker() { - "postgres://heartbeat@db/heartbeat".into() + String::from("postgres://heartbeat@db/heartbeat") } else { - "postgres://postgres@localhost/postgres".into() + String::from("postgres://postgres@localhost/postgres") } }); @@ -242,11 +273,11 @@ impl<'a> Merge<'a> { config_field!(secret_key, String, String::new()); - config_field!(repo, String, "https://github.com/lmaotrigine/heartbeat".into()); + config_field!(repo, String, String::from("https://github.com/lmaotrigine/heartbeat")); - config_field!(server_name, String, "Some person's heartbeat".into()); + config_field!(server_name, String, String::from("Some person's heartbeat")); - config_field!(live_url, String, "http://127.0.0.1:6060".into()); + config_field!(live_url, String, String::from("http://127.0.0.1:6060")); config_field!( bind, @@ -308,15 +339,9 @@ impl Config { // just an empty table toml::Value::Table(toml::map::Map::new()) }; - let profile_override = if cfg!(debug_assertions) { - toml_config.get("debug") - } else { - toml_config.get("release") - }; let config = Merge { cli, - default_toml: &toml_config, - profile_toml: profile_override, + toml: &toml_config, }; config.try_into() }