/*
* Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
*
* 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<String>,
}
#[derive(Deserialize)]
pub struct Config {
pub interface: String,
pub port: u16,
pub services: HashMap<String, Box<dyn ServiceConfig>>,
pub notifiers: HashMap<String, NotifierConfig>,
}
impl Config {
pub async fn from_str(s: &str) -> Result<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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());
}
}