/* * Copyright (C) 2025 Jonni Liljamo * * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for * more information. */ use std::{collections::HashMap, net::IpAddr, str::FromStr}; use serde::Deserialize; use tokio::{fs::File, io::AsyncReadExt}; #[derive(Debug)] pub enum ConfigError { InvalidInterface, NonExistentService(String), } impl std::error::Error for ConfigError {} impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidInterface => write!(f, "invalid interface"), Self::NonExistentService(s) => write!(f, "service '{}' does not exist", s), } } } #[typetag::serde(tag = "type")] pub trait ServiceConfig { fn as_any(&self) -> &dyn std::any::Any; } #[derive(Clone, Deserialize)] pub struct NotifierConfig { pub token: String, pub services: Vec, } #[derive(Deserialize)] pub struct Config { pub interface: String, pub port: u16, pub services: HashMap>, pub notifiers: HashMap, } impl Config { pub async fn from_str(s: &str) -> Result> { let config: Config = toml::from_str(s)?; // Verify interface if IpAddr::from_str(&config.interface).is_err() { return Err(Box::new(ConfigError::InvalidInterface)); } // Verify notifiers target services that exist for notifier in config.notifiers.values() { for service in ¬ifier.services { if !config.services.contains_key(service) { return Err(Box::new(ConfigError::NonExistentService(service.into()))); } } } Ok(config) } pub async fn from_path(path: &str) -> Result> { let mut content: String = String::new(); File::open(path).await?.read_to_string(&mut content).await?; Self::from_str(&content).await } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn basic_ok() { assert!( Config::from_str( r#" interface = "0.0.0.0" port = 8080 "# ) .await .is_ok() ); } #[tokio::test] async fn invalid_interface() { assert!( Config::from_str( r#" interface = "0.0.0.a" port = 8080 "# ) .await .is_err() ); } }