use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use firewatch_alerts::NostrConfig;
use firewatch_connectors::NodeConfig;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub watch: WatchConfig,
#[serde(default)]
pub alerts: AlertsConfig,
#[serde(default)]
pub nodes: Vec<NodeConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct WatchConfig {
pub interval_secs: u64,
pub listen: String,
pub dashboard_dir: Option<PathBuf>,
}
impl Default for WatchConfig {
fn default() -> Self {
WatchConfig {
interval_secs: 30,
listen: "127.0.0.1:7070".into(),
dashboard_dir: None,
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AlertsConfig {
pub nostr: Option<NostrConfig>,
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() {
anyhow::bail!(
"no config at {}. Run `firewatch init` to set one up, or pass --config",
path.display()
);
}
let text =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let config: Config =
toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
config
.check_names()
.with_context(|| format!("in {}", path.display()))?;
Ok(config)
}
fn check_names(&self) -> Result<()> {
let mut seen = std::collections::HashSet::new();
for (i, node) in self.nodes.iter().enumerate() {
let name = node.name();
if name.trim().is_empty() {
anyhow::bail!("node {} has no name", i + 1);
}
if !seen.insert(name) {
anyhow::bail!("two nodes are named \"{name}\"; give each its own name");
}
}
Ok(())
}
}
pub fn resolve(flag: Option<PathBuf>) -> PathBuf {
flag.unwrap_or_else(|| {
let here = PathBuf::from("firewatch.toml");
if here.exists() {
here
} else {
default_path()
}
})
}
pub fn default_path() -> PathBuf {
config_dir().join("firewatch.toml")
}
pub fn config_dir() -> PathBuf {
std::env::var_os("XDG_CONFIG_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.unwrap_or_else(|| PathBuf::from("."))
.join("firewatch")
}
#[cfg(test)]
mod tests {
use super::*;
fn names(names: &[&str]) -> Result<()> {
let mut text = String::new();
for n in names {
text.push_str(&format!(
"[[nodes]]\nkind = \"cln\"\nname = \"{n}\"\nurl = \"http://127.0.0.1:1\"\nrune = \"x\"\n"
));
}
toml::from_str::<Config>(&text).unwrap().check_names()
}
#[test]
fn node_names_must_be_set_and_unique() {
assert!(names(&["a", "b"]).is_ok());
let dup = names(&["a", "b", "a"]).unwrap_err().to_string();
assert!(dup.contains("two nodes are named \"a\""), "{dup}");
let empty = names(&["a", " "]).unwrap_err().to_string();
assert_eq!(empty, "node 2 has no name");
}
}