1use serde::de::{self, Unexpected, Visitor};
40use serde::{Deserialize, Deserializer};
41use std::collections::BTreeMap;
42use std::fmt;
43use std::io;
44use std::path::{Path, PathBuf};
45
46#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
48#[serde(deny_unknown_fields)]
49pub struct File {
50 #[serde(default)]
51 pub defaults: Section,
52 #[serde(default, rename = "session")]
54 pub sessions: BTreeMap<String, Section>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Spec {
60 Port(u32),
61 Text(String),
62}
63
64impl Spec {
65 pub fn as_text(&self) -> String {
66 match self {
67 Self::Port(n) => n.to_string(),
68 Self::Text(s) => s.clone(),
69 }
70 }
71}
72
73impl<'de> Deserialize<'de> for Spec {
79 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
80 struct SpecVisitor;
81
82 impl Visitor<'_> for SpecVisitor {
83 type Value = Spec;
84
85 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str("a monitor port, or \"port:echo_port\", \"unix\", or 0")
87 }
88
89 fn visit_u64<E: de::Error>(self, n: u64) -> Result<Spec, E> {
90 u32::try_from(n)
91 .map(Spec::Port)
92 .map_err(|_| E::invalid_value(Unexpected::Unsigned(n), &self))
93 }
94
95 fn visit_i64<E: de::Error>(self, n: i64) -> Result<Spec, E> {
96 u32::try_from(n)
97 .map(Spec::Port)
98 .map_err(|_| E::invalid_value(Unexpected::Signed(n), &self))
99 }
100
101 fn visit_str<E: de::Error>(self, s: &str) -> Result<Spec, E> {
102 Ok(Spec::Text(s.to_owned()))
103 }
104 }
105
106 de.deserialize_any(SpecVisitor)
107 }
108}
109
110#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
113#[serde(deny_unknown_fields)]
114pub struct Section {
115 pub monitor: Option<Spec>,
116 pub ssh_args: Option<Vec<String>>,
117 pub ssh_path: Option<PathBuf>,
118 pub poll: Option<u64>,
119 pub first_poll: Option<u64>,
120 pub gatetime: Option<u64>,
121 pub maxstart: Option<i64>,
122 pub maxlifetime: Option<u64>,
123 pub message: Option<String>,
124 pub pidfile: Option<PathBuf>,
125 pub monitor_host: Option<String>,
126 pub kill_timeout: Option<u64>,
127 pub log: Option<String>,
129 pub log_format: Option<String>,
131 pub loglevel: Option<String>,
133}
134
135impl File {
136 pub fn section(&self, session: Option<&str>) -> Result<Section, String> {
138 let mut merged = self.defaults.clone();
139 if let Some(name) = session {
140 let Some(s) = self.sessions.get(name) else {
141 return Err(format!(
142 "no session named \"{name}\" in the config file{}",
143 self.hint()
144 ));
145 };
146 merged.overlay(s);
147 }
148 Ok(merged)
149 }
150
151 pub fn session_names(&self) -> Vec<&str> {
153 self.sessions.keys().map(String::as_str).collect()
154 }
155
156 fn hint(&self) -> String {
157 let names = self.session_names();
158 if names.is_empty() {
159 " (it defines none)".to_owned()
160 } else {
161 format!(" (it has: {})", names.join(", "))
162 }
163 }
164}
165
166impl Section {
167 fn overlay(&mut self, other: &Section) {
169 macro_rules! take {
170 ($($field:ident),* $(,)?) => {
171 $( if other.$field.is_some() { self.$field = other.$field.clone(); } )*
172 };
173 }
174 take!(
175 monitor,
176 ssh_args,
177 ssh_path,
178 poll,
179 first_poll,
180 gatetime,
181 maxstart,
182 maxlifetime,
183 message,
184 pidfile,
185 monitor_host,
186 kill_timeout,
187 log,
188 log_format,
189 loglevel,
190 );
191 }
192}
193
194pub fn parse(text: &str) -> Result<File, String> {
197 toml::from_str(text).map_err(|e| e.to_string())
198}
199
200pub fn load(path: &Path) -> Result<File, String> {
202 match std::fs::read_to_string(path) {
203 Ok(text) => parse(&text).map_err(|e| format!("{}: {e}", path.display())),
204 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(File::default()),
205 Err(e) => Err(format!("{}: {e}", path.display())),
206 }
207}