use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use navi_notifier_core::RuleConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub general: General,
pub github: GitHubConfig,
pub slack: SlackConfig,
pub rules: RuleConfig,
pub routes: Vec<RouteConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct General {
pub poll_interval_secs: u64,
pub log_level: String,
pub utc_offset_minutes: i32,
}
impl Default for General {
fn default() -> Self {
Self {
poll_interval_secs: 60,
log_level: "info".into(),
utc_offset_minutes: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GitHubConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub api_base: Option<String>,
}
impl Default for GitHubConfig {
fn default() -> Self {
Self {
enabled: true,
token_env: "NAVI_GITHUB_TOKEN".into(),
token: None,
api_base: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SlackConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub dm_to: String,
}
impl Default for SlackConfig {
fn default() -> Self {
Self {
enabled: true,
token_env: "NAVI_SLACK_TOKEN".into(),
token: None,
dm_to: "self".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteConfig {
pub source: String,
pub notifier: String,
}
impl GitHubConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("github", self.token.as_deref(), &self.token_env)
}
}
impl SlackConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("slack", self.token.as_deref(), &self.token_env)
}
}
fn resolve_secret(what: &str, inline: Option<&str>, env_var: &str) -> Result<String> {
if let Some(tok) = inline.filter(|t| !t.is_empty()) {
return Ok(tok.to_string());
}
let val = std::env::var(env_var).map_err(|_| {
anyhow!("{what} token not found: set env var `{env_var}` (or the inline `token` field)")
})?;
if val.is_empty() {
return Err(anyhow!("{what} token env var `{env_var}` is empty"));
}
Ok(val)
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config at {}", path.display()))?;
let cfg: Config = toml::from_str(&text)
.with_context(|| format!("parsing config at {}", path.display()))?;
Ok(cfg)
}
pub fn engine_routes(&self) -> Vec<navi_notifier_core::Route> {
self.routes
.iter()
.map(|r| navi_notifier_core::Route {
source: r.source.clone(),
notifier: r.notifier.clone(),
})
.collect()
}
}
pub fn resolve_config_path(explicit: Option<PathBuf>) -> Result<PathBuf> {
if let Some(p) = explicit {
return Ok(p);
}
let dirs = directories::ProjectDirs::from("dev", "navi", "navi")
.ok_or_else(|| anyhow!("could not determine a config directory for this platform"))?;
Ok(dirs.config_dir().join("config.toml"))
}
pub fn resolve_state_path() -> Result<PathBuf> {
let dirs = directories::ProjectDirs::from("dev", "navi", "navi")
.ok_or_else(|| anyhow!("could not determine a data directory for this platform"))?;
Ok(dirs.data_dir().join("navi.sqlite3"))
}