use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::shell;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CommandLine {
Shell(String),
Exec(Vec<String>),
}
impl CommandLine {
#[must_use]
pub fn argv(&self) -> Vec<String> {
match self {
CommandLine::Shell(text) => vec!["/bin/sh".into(), "-c".into(), text.clone()],
CommandLine::Exec(args) => args.clone(),
}
}
#[must_use]
pub fn shell_line(&self) -> String {
match self {
CommandLine::Shell(text) => text.clone(),
CommandLine::Exec(args) => args.iter().map(|a| shell::quote(a)).collect::<Vec<_>>().join(" "),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
CommandLine::Shell(text) => text.trim().is_empty(),
CommandLine::Exec(args) => args.first().is_none_or(|a| a.trim().is_empty()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Healthcheck {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub test: Option<CommandLine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub http: Option<HttpCheck>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retries: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_period: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HttpCheck {
pub port: u16,
#[serde(default = "root")]
pub path: String,
}
fn root() -> String {
"/".into()
}
#[must_use]
pub fn seconds(duration: &str) -> Option<u32> {
let mut total: u32 = 0;
let mut number = String::new();
for c in duration.trim().chars() {
if c.is_ascii_digit() {
number.push(c);
continue;
}
let unit = match c {
'h' => 3600,
'm' => 60,
's' => 1,
_ => return None,
};
let n: u32 = number.parse().ok()?;
total = total.checked_add(n.checked_mul(unit)?)?;
number.clear();
}
(number.is_empty() && !duration.trim().is_empty()).then_some(total)
}
#[cfg(test)]
mod tests {
use super::{CommandLine, seconds};
#[test]
fn durations_read_in_seconds() {
assert_eq!(seconds("5s"), Some(5));
assert_eq!(seconds("1m30s"), Some(90));
assert_eq!(seconds("2h"), Some(7200));
assert_eq!(seconds("0s"), Some(0));
for bad in ["", "5", "s", "5x", "1.5s", "-1s"] {
assert_eq!(seconds(bad), None, "{bad}");
}
}
#[test]
fn a_command_as_arguments_or_a_line() {
let text = CommandLine::Shell("echo $HOME && true".into());
assert_eq!(text.argv(), ["/bin/sh", "-c", "echo $HOME && true"]);
assert_eq!(text.shell_line(), "echo $HOME && true");
let args = CommandLine::Exec(vec!["nginx".into(), "-g".into(), "daemon off;".into()]);
assert_eq!(args.argv(), ["nginx", "-g", "daemon off;"]);
assert_eq!(args.shell_line(), "'nginx' '-g' 'daemon off;'");
assert!(CommandLine::Exec(Vec::new()).is_empty() && CommandLine::Shell(" ".into()).is_empty());
}
}