naenae 0.1.0

Monitor external command output and send Discord webhook notifications.
use std::path::PathBuf;

use anyhow::{Context, Result};
use serde::Deserialize;
use tokio::fs;

use crate::rules::StreamSelector;

#[derive(Debug, Deserialize)]
pub struct Config {
    pub discord: DiscordConfig,
    pub monitor: MonitorConfig,
    pub run: Option<RunConfig>,
    pub attach: Option<AttachConfig>,
    #[serde(default)]
    pub rules: Vec<RuleConfig>,
}

#[derive(Debug, Deserialize)]
pub struct DiscordConfig {
    pub webhook_url: String,
    pub bot_name: Option<String>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct MonitorConfig {
    pub name: String,
    pub notify_on_start: Option<bool>,
    pub notify_on_finish: Option<bool>,
    pub notify_on_fail: Option<bool>,
    pub include_last_output_in_fail_message: Option<bool>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct RunConfig {
    pub command: String,
    #[serde(default)]
    pub args: Vec<String>,
    pub cwd: Option<PathBuf>,
    pub pty: Option<bool>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct AttachConfig {
    pub log_file: Option<PathBuf>,
    pub start_at_end: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub struct RuleConfig {
    pub name: String,
    pub pattern: Option<String>,
    #[serde(default)]
    pub patterns: Vec<String>,
    #[serde(default = "default_stream")]
    pub stream: StreamSelector,
    pub notify_template: Option<String>,
    pub cooldown_secs: Option<u64>,
    pub max_notifications: Option<u32>,
}

fn default_stream() -> StreamSelector {
    StreamSelector::Both
}

pub async fn load_config(path: &PathBuf) -> Result<Config> {
    let contents = fs::read_to_string(path)
        .await
        .with_context(|| format!("failed to read config at {}", path.display()))?;
    let mut config: Config = toml::from_str(&contents).context("failed to parse TOML config")?;
    let config_dir = path
        .parent()
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));

    if let Some(run) = config.run.as_mut()
        && let Some(cwd) = run.cwd.as_mut()
        && cwd.is_relative()
    {
        *cwd = config_dir.join(&*cwd);
    }

    Ok(config)
}