Skip to content

Commit

Permalink
add profile overloads to TOML
Browse files Browse the repository at this point in the history
  • Loading branch information
lmaotrigine committed Oct 30, 2023
1 parent d78e809 commit c307842
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 23 deletions.
10 changes: 10 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ url = ""
# - "long_absences" log absences longer than 1 hour
# - "none" don't log anything
level = "none"

# override some values for debug builds for easier testing.

[debug]
bind = "127.0.0.1:6061"
live_url = "http://localhost:6061"

# nested keys work as expected
[debug.database]
dsn = "postgresql://heartbeat@localhost/heartbeat"
3 changes: 3 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Configuration for the server is hierarchical and read from the following sources

- A TOML configuration file located at `config.toml` in the current working directory, or the location specified by the
environment varable `HEARTBEAT_CONFIG_FILE` or the command line option `-c`/`--config-file`.
This configuration can have "profile" overloads. For example `release.foo` takes precedence over `foo` in release
builds and `debug.foo` takes precendence over `foo` in debug builds. This is checked using `cfg(debug_assertions)` so
if you fiddle with that, your mileage may vary.
- Environment variables prefixed with `HEARTBEAT_`.
- Command line options.

Expand Down
65 changes: 42 additions & 23 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,6 @@ use std::{
use toml::{self, de::Error as TomlDeError};
use tracing::info;

#[derive(Deserialize, Default)]
struct TomlConfig {
database: Option<Database>,
#[cfg(feature = "webhook")]
webhook: Option<Webhook>,
secret_key: Option<String>,
repo: Option<String>,
server_name: Option<String>,
live_url: Option<String>,
bind: Option<SocketAddr>,
static_dir: Option<PathBuf>,
}

#[derive(Debug, Parser)]
#[clap(about, author, version = crate::VERSION)]
#[clap(help_template = r"{name} {version}
Expand Down Expand Up @@ -173,14 +160,28 @@ impl From<TomlDeError> for Error {
}
}

// this is a bit of a cluster fuck
// but 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> {
let value: Result<_, Error> = if let Some(ref $field) = self.cli.$field {
Ok($field.to_owned())
} else {
if let Some(ref outer) = self.toml.$first {
Ok(outer.$second.to_owned())
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)))?
}
Expand All @@ -191,10 +192,17 @@ macro_rules! config_field {
($field:ident, $type:ty$(, $default:expr)?) => {
pub fn $field(&self) -> Result<$type, Error> {
let value = if let Some(ref $field) = self.cli.$field {
Ok($field.to_owned())
Ok::<_, Error>($field.to_owned())
} else {
if let Some(ref $field) = self.toml.$field {
Ok($field.to_owned())
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)))
}
Expand All @@ -204,9 +212,10 @@ macro_rules! config_field {
};
}

struct Merge {
struct Merge<'a> {
cli: Cli,
toml: TomlConfig,
default_toml: &'a toml::Value,
profile_toml: Option<&'a toml::Value>,
}

#[inline]
Expand All @@ -216,7 +225,7 @@ fn is_docker() -> bool {
dockerenv.exists() || (read_to_string(path).map_or(false, |s| s.lines().any(|l| l.contains("docker"))))
}

impl Merge {
impl<'a> Merge<'a> {
config_field!(database.dsn, database_dsn, String, {
if is_docker() {
"postgres://heartbeat@db/heartbeat".into()
Expand Down Expand Up @@ -296,9 +305,19 @@ impl Config {
} else if fail_on_not_exists {
return Err(Error::InvalidConfigPath(config_path.to_path_buf()));
} else {
TomlConfig::default()
// 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,
};
let config = Merge { cli, toml: toml_config };
config.try_into()
}
}

0 comments on commit c307842

Please sign in to comment.