use serde::de::{self, Unexpected, Visitor};
use serde::{Deserialize, Deserializer};
use std::collections::BTreeMap;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct File {
#[serde(default)]
pub defaults: Section,
#[serde(default, rename = "session")]
pub sessions: BTreeMap<String, Section>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Spec {
Port(u32),
Text(String),
}
impl Spec {
pub fn as_text(&self) -> String {
match self {
Self::Port(n) => n.to_string(),
Self::Text(s) => s.clone(),
}
}
}
impl<'de> Deserialize<'de> for Spec {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct SpecVisitor;
impl Visitor<'_> for SpecVisitor {
type Value = Spec;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a monitor port, or \"port:echo_port\", \"unix\", or 0")
}
fn visit_u64<E: de::Error>(self, n: u64) -> Result<Spec, E> {
u32::try_from(n)
.map(Spec::Port)
.map_err(|_| E::invalid_value(Unexpected::Unsigned(n), &self))
}
fn visit_i64<E: de::Error>(self, n: i64) -> Result<Spec, E> {
u32::try_from(n)
.map(Spec::Port)
.map_err(|_| E::invalid_value(Unexpected::Signed(n), &self))
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Spec, E> {
Ok(Spec::Text(s.to_owned()))
}
}
de.deserialize_any(SpecVisitor)
}
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Section {
pub monitor: Option<Spec>,
pub ssh_args: Option<Vec<String>>,
pub ssh_path: Option<PathBuf>,
pub poll: Option<u64>,
pub first_poll: Option<u64>,
pub gatetime: Option<u64>,
pub maxstart: Option<i64>,
pub maxlifetime: Option<u64>,
pub message: Option<String>,
pub pidfile: Option<PathBuf>,
pub monitor_host: Option<String>,
pub kill_timeout: Option<u64>,
pub log: Option<String>,
pub log_format: Option<String>,
pub loglevel: Option<String>,
}
impl File {
pub fn section(&self, session: Option<&str>) -> Result<Section, String> {
let mut merged = self.defaults.clone();
if let Some(name) = session {
let Some(s) = self.sessions.get(name) else {
return Err(format!(
"no session named \"{name}\" in the config file{}",
self.hint()
));
};
merged.overlay(s);
}
Ok(merged)
}
pub fn session_names(&self) -> Vec<&str> {
self.sessions.keys().map(String::as_str).collect()
}
fn hint(&self) -> String {
let names = self.session_names();
if names.is_empty() {
" (it defines none)".to_owned()
} else {
format!(" (it has: {})", names.join(", "))
}
}
}
impl Section {
fn overlay(&mut self, other: &Section) {
macro_rules! take {
($($field:ident),* $(,)?) => {
$( if other.$field.is_some() { self.$field = other.$field.clone(); } )*
};
}
take!(
monitor,
ssh_args,
ssh_path,
poll,
first_poll,
gatetime,
maxstart,
maxlifetime,
message,
pidfile,
monitor_host,
kill_timeout,
log,
log_format,
loglevel,
);
}
}
pub fn parse(text: &str) -> Result<File, String> {
toml::from_str(text).map_err(|e| e.to_string())
}
pub fn load(path: &Path) -> Result<File, String> {
match std::fs::read_to_string(path) {
Ok(text) => parse(&text).map_err(|e| format!("{}: {e}", path.display())),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(File::default()),
Err(e) => Err(format!("{}: {e}", path.display())),
}
}