Skip to content

Commit

Permalink
Log loaded configuration at startup (#529)
Browse files Browse the repository at this point in the history
  • Loading branch information
flosse authored Jun 6, 2024
1 parent 118704a commit 3818797
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 47 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ duration-str = { version = "0.7.1", default-features = false, features = ["serde
env_logger = "0.11.3"
log = "0.4.21"
serde = { version = "1.0.203", features = ["derive"] }
thiserror = "1.0.61"
time = "0.3.36"
tokio = "1.38.0"
toml = "0.8.14"
Expand Down
2 changes: 1 addition & 1 deletion ofdb-core/src/usecases/send_update_reminders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ where
Ok(())
}

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RecipientRole {
Owner,
Scout,
Expand Down
168 changes: 138 additions & 30 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use anyhow::{anyhow, Result};
use ofdb_core::usecases::RecipientRole;
use ofdb_entities::email::EmailAddress;
use std::{
collections::HashSet,
env, fs,
io::ErrorKind,
fmt, fs, io,
path::{Path, PathBuf},
time::Duration,
};

mod raw;
use anyhow::anyhow;
use thiserror::Error;

const DEFAULT_CONFIG_FILE_NAME: &str = "openfairdb.toml";
use ofdb_core::usecases::RecipientRole;
use ofdb_entities::email::EmailAddress;

const ENV_NAME_DB_URL: &str = "DATABASE_URL";
mod raw;

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Config {
pub db: Db,
pub entries: Entries,
Expand All @@ -24,33 +24,59 @@ pub struct Config {
pub reminders: Reminders,
}

#[derive(Debug, Error)]
pub enum LoadError {
#[error("Config file not found")]
NotFound,

#[error(transparent)]
Toml(#[from] toml::de::Error),

#[error(transparent)]
Other(#[from] anyhow::Error),
}

impl Config {
pub fn try_load_from_file_or_default<P: AsRef<Path>>(file_path: Option<P>) -> Result<Self> {
let file_path: &Path = file_path.as_ref().map(|p| p.as_ref()).unwrap_or_else(|| {
log::info!("No configuration file specified. load {DEFAULT_CONFIG_FILE_NAME}");
Path::new(DEFAULT_CONFIG_FILE_NAME)
});
pub fn try_load_from_file<P: AsRef<Path>>(file_path: P) -> Result<Self, LoadError> {
let raw_config = try_load_raw_config_from_file(file_path)?;
let cfg = Self::try_from(raw_config)?;
Ok(cfg)
}

let raw_config = match fs::read_to_string(file_path) {
Ok(cfg_string) => toml::from_str(&cfg_string)?,
Err(err) => match err.kind() {
ErrorKind::NotFound => {
pub fn try_load_from_file_or_default<P: AsRef<Path>>(file_path: P) -> anyhow::Result<Self> {
match Self::try_load_from_file(file_path.as_ref()) {
Ok(cfg) => Ok(cfg),
Err(err) => match err {
LoadError::NotFound => {
log::info!(
"{DEFAULT_CONFIG_FILE_NAME} not found => load default configuration."
"Configuration file {} not found: load default configuration.",
file_path.as_ref().display()
);
Ok(raw::Config::default())
Ok(Self::default())
}
_ => Err(err),
}?,
};
let mut cfg = Self::try_from(raw_config)?;
if let Ok(db_url) = env::var(ENV_NAME_DB_URL) {
cfg.db.conn_sqlite = db_url;
_ => Err(err.into()),
},
}
Ok(cfg)
}
}

impl Default for Config {
fn default() -> Self {
Self::try_from(raw::Config::default()).expect("default config")
}
}

fn try_load_raw_config_from_file<P: AsRef<Path>>(file_path: P) -> Result<raw::Config, LoadError> {
let cfg_string = fs::read_to_string(file_path).map_err(|err| match err.kind() {
io::ErrorKind::NotFound => LoadError::NotFound,
_ => LoadError::Other(err.into()),
})?;
let raw_config = toml::from_str(&cfg_string)?;
Ok(raw_config)
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Db {
/// SQLite connection
pub conn_sqlite: String,
Expand All @@ -59,28 +85,48 @@ pub struct Db {
pub index_dir: Option<PathBuf>,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Geocoding {
pub gateway: Option<GeocodingGateway>,
}

#[cfg_attr(test, derive(PartialEq))]
pub enum GeocodingGateway {
OpenCage { api_key: String },
}

impl fmt::Debug for GeocodingGateway {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GeocodingGateway::OpenCage { api_key: _ } => {
f.debug_struct("OpenCage").field("api_key", &"***").finish()
}
}
}
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Entries {
pub accepted_licenses: HashSet<String>,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct WebServer {
pub protect_with_captcha: bool,
pub enable_cors: bool,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Email {
pub gateway: Option<EmailGateway>,
}

#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum EmailGateway {
MailGun {
api_base_url: String, // TODO: use url::Url
Expand All @@ -98,6 +144,37 @@ pub enum EmailGateway {
},
}

impl fmt::Debug for EmailGateway {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
EmailGateway::MailGun {
api_base_url,
api_key: _,
domain,
sender_address,
} => f
.debug_struct("MailGun")
.field("api_base_url", &api_base_url)
.field("api_key", &"***")
.field("domain", &domain)
.field("sender_address", &sender_address)
.finish(),

EmailGateway::Sendmail { sender_address } => f
.debug_struct("Sendmail")
.field("sender_address", &sender_address)
.finish(),

EmailGateway::EmailToJsonFile { dir } => f
.debug_struct("EmailToJsonFile")
.field("dir", &dir)
.finish(),
}
}
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Reminders {
pub task_interval_time: Duration,
pub send_max: u32,
Expand All @@ -108,17 +185,21 @@ pub struct Reminders {
pub token_expire_in: Duration,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ScoutReminders {
pub not_updated_for: Duration,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct OwnerReminders {
pub not_updated_for: Duration,
}

impl TryFrom<raw::Config> for Config {
type Error = anyhow::Error;
fn try_from(from: raw::Config) -> Result<Self> {
fn try_from(from: raw::Config) -> anyhow::Result<Self> {
let raw::Config {
db,
geocoding,
Expand Down Expand Up @@ -241,7 +322,7 @@ impl TryFrom<raw::Config> for Config {
let send_bcc = if let Some(bcc) = send_bcc {
bcc.into_iter()
.map(|a| a.parse::<EmailAddress>())
.collect::<Result<Vec<_>, _>>()?
.collect::<anyhow::Result<Vec<_>, _>>()?
} else {
vec![]
};
Expand Down Expand Up @@ -312,7 +393,34 @@ mod tests {

#[test]
fn load_default_config() {
let file: Option<&Path> = None;
let _: Config = Config::try_load_from_file_or_default(file).unwrap();
let file = Path::new("");
let cfg = Config::try_load_from_file_or_default(file).unwrap();
assert_eq!(cfg, Config::default());
assert!(cfg.reminders.send_to.is_empty());
assert!(cfg.reminders.send_bcc.is_empty());
}

#[test]
fn hide_api_key_of_geo_gateway() {
let x = GeocodingGateway::OpenCage {
api_key: "123".to_string(),
};
let d = format!("{x:?}");
assert_eq!(r#"OpenCage { api_key: "***" }"#, d);
}

#[test]
fn hide_api_key_of_mailgun_gateway() {
let x = EmailGateway::MailGun {
api_base_url: "x".to_string(),
domain: "y".to_string(),
sender_address: "[email protected]".parse().unwrap(),
api_key: "123".to_string(),
};
let d = format!("{x:?}");
assert_eq!(
r#"MailGun { api_base_url: "x", api_key: "***", domain: "y", sender_address: EmailAddress { address: "[email protected]", display_name: None } }"#,
d
);
}
}
1 change: 1 addition & 0 deletions src/config/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ mod tests {
assert!(cfg.task_interval_time.is_some());
assert!(cfg.send_max.is_some());
assert!(cfg.send_to.is_none());
assert!(cfg.send_bcc.is_none());
assert!(cfg.scouts.is_some());
assert!(cfg.owners.is_some());
assert!(cfg.token_expire_in.is_some());
Expand Down
59 changes: 44 additions & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ where
{
let events = repo.all_events_chronologically()?;
for mut e in events {
if let Some(ref mut loc) = e.location {
if let Some(ref addr) = loc.address {
if let Some((lat, lng)) = geo.resolve_address_lat_lng(addr) {
if let Ok(pos) = MapPoint::try_from_lat_lng_deg(lat, lng) {
if pos.is_valid() {
if let Err(err) = repo.update_event(&e) {
log::warn!("Failed to update location of event {}: {}", e.id, err);
} else {
log::info!("Updated location of event {}", e.id);
}
}
}
}
let Some(ref mut loc) = e.location else {
continue;
};
let Some(ref addr) = loc.address else {
continue;
};
let Some((lat, lng)) = geo.resolve_address_lat_lng(addr) else {
continue;
};
let Ok(pos) = MapPoint::try_from_lat_lng_deg(lat, lng) else {
continue;
};
if pos.is_valid() {
if let Err(err) = repo.update_event(&e) {
log::warn!("Failed to update location of event {}: {err}", e.id);
} else {
log::info!("Updated location of event {}", e.id);
}
}
}
Expand All @@ -59,13 +63,38 @@ enum Command {
FixEventAddressLocation,
}

const ENV_NAME_DB_URL: &str = "DATABASE_URL";
const ENV_NAME_RUST_LOG: &str = "RUST_LOG";

const DEFAULT_CONFIG_FILE_NAME: &str = "openfairdb.toml";

const FALLBACK_RUST_LOG_CONFIG: &str = "info,tantivy=warn";

#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
env_logger::init();
dotenv().ok();
if env::var(ENV_NAME_RUST_LOG) == Err(env::VarError::NotPresent) {
env::set_var(ENV_NAME_RUST_LOG, FALLBACK_RUST_LOG_CONFIG);
}
env_logger::init();

let args = Args::parse();

let cfg = config::Config::try_load_from_file_or_default(args.config_file)?;
let mut cfg = match args.config_file {
Some(file_path) => config::Config::try_load_from_file(file_path)?,
None => {
log::info!("No configuration file specified: load {DEFAULT_CONFIG_FILE_NAME}");
config::Config::try_load_from_file_or_default(DEFAULT_CONFIG_FILE_NAME)?
}
};

if let Ok(db_url) = env::var(ENV_NAME_DB_URL) {
log::info!("Use DB connection {db_url} defined by {ENV_NAME_DB_URL}");
cfg.db.conn_sqlite = db_url;
}

log::info!("Start server with the following config:\n {cfg:#?}");

let config::Db {
conn_sqlite,
conn_pool_size,
Expand Down
2 changes: 1 addition & 1 deletion src/recurring_reminder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub async fn run(
cfg: config::Reminders,
) {
if cfg.send_to.is_empty() {
log::info!("Do not send recurring reminders");
log::info!("No recipient defined in `send_to`: do not send recurring reminders");
return;
}

Expand Down

0 comments on commit 3818797

Please sign in to comment.