use crate::dyndns_service::DynDnsProvider; use crate::error::{AppError, AppResult}; use crate::ip_service::IpServiceProvider; use fqdn::FQDN; use serde::{Deserialize, Serialize}; use serde_with::DisplayFromStr; use serde_with::serde_as; use std::path::{Path, PathBuf}; use std::{fs, io}; use log::warn; use strum::Display; #[derive(Debug, Deserialize, Serialize)] #[serde(transparent)] pub struct Config { pub networks: Vec, } impl Config { pub fn load(paths: Vec) -> AppResult { let mut index = 1; for path in &paths { let content = fs::read_to_string(&path); match content { Ok(value) => { match serde_json::from_str(&value) { Ok(config) => { return Ok(config) }, Err(error) => { if index == paths.len() { return Err(AppError::ConfigParseError { source: error, path: path.to_path_buf(), })} warn!("{}", error); } } } Err(e) => { let error = match e.kind() { io::ErrorKind::NotFound => AppError::ConfigFileNotFound(path.clone()), _ => AppError::IoError(e), }; if index == paths.len() { return Err(error); } warn!("{}", error); }, } index += 1; } Err(AppError::ConfigFileNotProvided) } } #[derive(Debug, Deserialize, Serialize)] pub struct WanConfig { pub interface: Option, #[serde(flatten)] pub dns_record: DnsRecord, pub providers: Vec, #[serde(default)] pub ip_service: IpServiceProvider, } #[derive(Debug, Display, Deserialize, Serialize, PartialEq)] pub enum DnsRecordType { A, #[serde(rename = "AAAA")] Aaaa, } #[serde_as] #[derive(Debug, Deserialize, Serialize)] pub struct DnsRecord { #[serde_as(as = "DisplayFromStr")] fqdn: FQDN, #[serde(default = "default_ttl")] ttl: u32, #[serde(default = "default_record_type")] record_type: DnsRecordType, } fn default_record_type() -> DnsRecordType { DnsRecordType::A } fn default_ttl() -> u32 { 300 } #[cfg(test)] mod tests { use super::*; use crate::dyndns_service::DynDnsProvider::Gandi; use crate::dyndns_service::gandi::GandiConfig; use crate::{ assert_config_file_not_found, assert_config_parse_error, assert_error, assert_io_error, test, }; use serde_json::json; use std::fs::read_dir; use std::path::PathBuf; use std::str::FromStr; test! { fn check_minimal_config() { let input = json!({ "fqdn": "dyn.domain.com", "providers": [ { "type": "GANDI", "api_key": "SOME-API-KEY", } ] }); let wan_config = serde_json::from_value::(input)?; assert_eq!( wan_config.dns_record.fqdn, FQDN::from_str("dyn.domain.com")? ); assert_eq!(wan_config.interface, None); assert_eq!(wan_config.providers.len(), 1); let expected = GandiConfig::new("SOME-API-KEY".to_string()); let actual = wan_config.providers.get(0).unwrap(); assert_eq!(&Gandi(expected), actual); } } test! { fn check_defaults_on_dns_record_deserialization() { let input = json!({ "fqdn": "dyn.mydomain.com", }); let dns_record = serde_json::from_value::(input)?; assert_eq!(dns_record.record_type, default_record_type()); assert_eq!(dns_record.ttl, default_ttl()); assert_eq!(dns_record.fqdn, FQDN::from_str("dyn.mydomain.com")?); } } test! { fn check_test_file_configs() { let path = Path::new("test"); for entry in read_dir(path)? { let entry = entry?; let test_file_path = entry.path(); if test_file_path.extension().unwrap_or_default() != "json" { continue; } Config::load(vec![test_file_path])?; } } } test! { fn check_chooses_fallback_config() { let configs = vec![PathBuf::from_str("test/unknown.json")?, PathBuf::from_str("test/also-unknown.json")?, PathBuf::from_str("test/config.bork")?, PathBuf::from_str("test/noop.json")?]; Config::load(configs)?; } } test! { fn missing_config() { let path = Path::new("test/unknown.yaml"); assert_config_file_not_found!(Config::load(vec![path.to_path_buf()]), path); } } test! { fn broken_config() { let path = Path::new("test/config.bork"); assert_config_parse_error!(Config::load(vec![path.to_path_buf()]), path); } } test! { fn inaccessible_config() { let path = Path::new("/root/secure-config.json"); assert_io_error!(Config::load(vec![path.to_path_buf()])); } } }