use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApplyGroup {
Live,
NextSpawn,
NeedsRespawn,
Structural,
}
const FIELDS: &[(&str, ApplyGroup)] = &[
("autorestart", ApplyGroup::Live),
("max_restarts", ApplyGroup::Live),
("min_uptime", ApplyGroup::Live),
("restart_delay", ApplyGroup::Live),
("exp_backoff_restart_delay", ApplyGroup::Live),
("stop_exit_codes", ApplyGroup::Live),
("kill_timeout", ApplyGroup::Live),
("graceful_timeout", ApplyGroup::Live),
("max_memory", ApplyGroup::Live),
("watch", ApplyGroup::Live),
("ignore_watch", ApplyGroup::Live),
("watch_delay", ApplyGroup::Live),
("watch_options", ApplyGroup::Live),
("cron_restart", ApplyGroup::Live),
("cron_timezone", ApplyGroup::Live),
("liveness_probe", ApplyGroup::Live),
("fold", ApplyGroup::Live),
("reuse_port", ApplyGroup::Live),
("action_timeout", ApplyGroup::Live),
("kill_signal", ApplyGroup::NextSpawn),
("listen_timeout", ApplyGroup::NextSpawn),
("readiness_probe", ApplyGroup::NextSpawn),
("autostart", ApplyGroup::NextSpawn),
("depends_on", ApplyGroup::NextSpawn),
("script", ApplyGroup::NeedsRespawn),
("args", ApplyGroup::NeedsRespawn),
("cwd", ApplyGroup::NeedsRespawn),
("interpreter", ApplyGroup::NeedsRespawn),
("env", ApplyGroup::NeedsRespawn),
("environment", ApplyGroup::NeedsRespawn),
("user", ApplyGroup::NeedsRespawn),
("group", ApplyGroup::NeedsRespawn),
("out_file", ApplyGroup::NeedsRespawn),
("err_file", ApplyGroup::NeedsRespawn),
("merge_logs", ApplyGroup::NeedsRespawn),
("channel", ApplyGroup::NeedsRespawn),
("stdin", ApplyGroup::NeedsRespawn),
("wait_ready", ApplyGroup::NeedsRespawn),
("shutdown_with_message", ApplyGroup::NeedsRespawn),
("name", ApplyGroup::Structural),
("instances", ApplyGroup::Structural),
("increment_var", ApplyGroup::Structural),
];
#[must_use]
pub fn apply_group(field: &str) -> ApplyGroup {
FIELDS
.iter()
.find(|(name, _)| *name == field)
.map_or(ApplyGroup::NeedsRespawn, |(_, group)| *group)
}
#[must_use]
pub fn is_classified(field: &str) -> bool {
FIELDS.iter().any(|(name, _)| *name == field)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResetDepth {
#[default]
None,
File,
Policy,
Env,
All,
}
#[cfg(test)]
mod tests {
use super::{ApplyGroup, apply_group, is_classified};
use crate::config::AppConfig;
#[test]
fn every_appconfig_field_has_a_group() {
let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
else {
panic!("AppConfig must serialize as an object");
};
let missing: Vec<&String> = fields.keys().filter(|k| !is_classified(k)).collect();
assert!(
missing.is_empty(),
"unclassified AppConfig fields: {missing:?}"
);
}
#[test]
fn kill_signal_reaches_the_next_spawn_not_the_next_kill() {
assert_eq!(apply_group("kill_signal"), ApplyGroup::NextSpawn);
assert_eq!(apply_group("kill_timeout"), ApplyGroup::Live);
assert_eq!(apply_group("graceful_timeout"), ApplyGroup::Live);
}
#[test]
fn shutdown_with_message_is_baked_into_the_child() {
assert_eq!(
apply_group("shutdown_with_message"),
ApplyGroup::NeedsRespawn
);
}
#[test]
fn depends_on_applies_at_the_next_spawn() {
assert_eq!(apply_group("depends_on"), ApplyGroup::NextSpawn);
assert!(is_classified("depends_on"));
}
#[test]
fn the_split_is_nineteen_five_fifteen_three() {
let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
else {
panic!("AppConfig must serialize as an object");
};
let count = |want: ApplyGroup| fields.keys().filter(|k| apply_group(k) == want).count();
assert_eq!(count(ApplyGroup::Live), 19, "Live");
assert_eq!(count(ApplyGroup::NextSpawn), 5, "NextSpawn");
assert_eq!(count(ApplyGroup::NeedsRespawn), 15, "NeedsRespawn");
assert_eq!(count(ApplyGroup::Structural), 3, "Structural");
}
}