use log::error;
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use std::{
net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf},
time::Duration,
};
use openweathermap_client::{
models::{City, CityId, Coord},
ClientOptions, Query,
};
use crate::ExporterError;
#[derive(Debug, Deserialize)]
pub struct ListenOptions {
#[serde(default = "ListenOptions::default_listen_address")]
pub address: IpAddr,
#[serde(default = "ListenOptions::default_listen_port")]
pub port: u16,
}
impl Default for ListenOptions {
fn default() -> Self {
Self {
address: ListenOptions::default_listen_address(),
port: ListenOptions::default_listen_port(),
}
}
}
impl ListenOptions {
fn default_listen_address() -> IpAddr {
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))
}
fn default_listen_port() -> u16 {
9001
}
}
#[serde_as] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExporterConfig {
#[serde(default = "ListenOptions::default")]
pub listen: ListenOptions,
#[serde(default = "ClientOptions::default")]
pub owm: ClientOptions,
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "ExporterConfig::default_poll_interval")]
#[serde(rename(deserialize = "poll_interval_seconds"))]
pub poll_interval: Duration,
#[serde(default = "ExporterConfig::default_max_calls_per_minute")]
pub max_calls_per_minute: u16,
#[serde(default = "Vec::new")]
pub cities: Vec<City>,
#[serde(default = "Vec::new")]
pub coordinates: Vec<Coord>,
#[serde(default = "Vec::new")]
pub locations: Vec<CityId>,
}
impl ExporterConfig {
fn default_poll_interval() -> Duration {
Duration::from_secs(60)
}
pub fn default_max_calls_per_minute() -> u16 {
60
}
pub fn load() -> Result<ExporterConfig, ExporterError> {
let path = Self::find_config_file()?;
let contents = read_from_path(&path)?;
let parsed: ExporterConfig = Self::parse(&path, &contents)?;
parsed.validate()?;
Ok(parsed)
}
fn parse(path: &Path, contents: &str) -> Result<ExporterConfig, ExporterError> {
match serde_yaml::from_str::<ExporterConfig>(contents) {
Ok(config) => Ok(config),
Err(e) => {
error!("Error {:?}", e);
Err(ExporterError::ConfigFormatError {
path: path.to_string_lossy().to_string(),
error: e,
})
}
}
}
fn find_config_file() -> Result<PathBuf, ExporterError> {
let candidates = vec!["owm_exporter.yaml", "owm_exporter.yml", "owm_exporter.json"];
let candidate_files: Vec<PathBuf> = vec![std::env::current_dir().ok(), dirs::home_dir()]
.iter()
.flatten()
.flat_map(|pb| candidates.iter().map(|file| pb.as_path().join(*file)))
.collect();
log::debug!("candidate files {:?}", candidate_files);
match candidate_files.iter().find(|f| f.exists() && f.is_file()) {
Some(pb) => Ok(pb.clone()),
None => Err(ExporterError::ConfigNotFound {
message: format!(
"Could not locate any of the following config files {}.",
join_paths(candidate_files, ", ")
),
}),
}
}
pub(crate) fn validate(&self) -> Result<(), ExporterError> {
if self.cities.len() + self.coordinates.len() + self.locations.len() == 0 {
return Err(ExporterError::ConfigValidationError {
message: "No cities or coordinates or locations were specified in the config".to_string(),
error: None,
});
}
if self.max_calls_per_minute == 0 {
return Err(ExporterError::ConfigValidationError {
message: "max_calls_per_minute must > 0".to_string(),
error: None,
});
}
match self.owm.validate() {
Ok(_) => Ok(()),
Err(e) => Err(ExporterError::ConfigValidationError {
message: "Owm Client Validation error".to_string(),
error: Some(e),
}),
}
}
pub(crate) fn query_iterator(&self) -> impl Iterator<Item = &dyn Query> {
let cities = self.cities.iter().map(|c| c as &dyn Query);
let coordinates = self.coordinates.iter().map(|c| c as &dyn Query);
let locations = self.locations.iter().map(|c| c as &dyn Query);
cities.chain(coordinates).chain(locations)
}
}
fn read_from_path(path: &PathBuf) -> Result<String, ExporterError> {
match std::fs::read_to_string(path) {
Ok(contents) => Ok(contents),
Err(e) => {
return Err(ExporterError::ConfigReadError {
path: path.to_string_lossy().to_string(),
error: e,
})
}
}
}
fn join_paths(pbs: Vec<PathBuf>, separator: &str) -> String {
pbs.iter()
.map(|pb| pb.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(separator)
}