use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::phase::ProcessPhase;
use crate::signal::SighupStrategy;
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct IdentitySpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_override: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DependsOn {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(default)]
pub must_reach: MustReachPhase,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
Default,
tatara_lisp::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", display, generate_unknown = "must-reach phase")]
pub enum MustReachPhase {
Running,
#[default]
Attested,
}
impl MustReachPhase {
pub const ALL: [Self; 2] = [Self::Running, Self::Attested];
pub const fn as_str(self) -> &'static str {
match self {
Self::Running => "Running",
Self::Attested => "Attested",
}
}
pub const fn as_process_phase(self) -> ProcessPhase {
match self {
Self::Running => ProcessPhase::Running,
Self::Attested => ProcessPhase::Attested,
}
}
}
impl From<MustReachPhase> for ProcessPhase {
fn from(v: MustReachPhase) -> Self {
v.as_process_phase()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignalPolicy {
#[serde(default = "default_sigterm_grace")]
pub sigterm_grace_seconds: u32,
#[serde(default = "default_true")]
pub sigkill_force: bool,
#[serde(default)]
pub sighup_strategy: SighupStrategy,
#[serde(default)]
pub start_suspended: bool,
}
impl Default for SignalPolicy {
fn default() -> Self {
Self {
sigterm_grace_seconds: default_sigterm_grace(),
sigkill_force: true,
sighup_strategy: SighupStrategy::default(),
start_suspended: false,
}
}
}
fn default_sigterm_grace() -> u32 {
480
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn must_reach_default_is_attested() {
assert_eq!(MustReachPhase::default(), MustReachPhase::Attested);
}
#[test]
fn signal_policy_defaults() {
let p = SignalPolicy::default();
assert_eq!(p.sigterm_grace_seconds, 480);
assert!(p.sigkill_force);
assert!(!p.start_suspended);
}
#[test]
fn must_reach_phase_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<MustReachPhase>();
}
#[test]
fn must_reach_phase_as_str_matches_serde() {
for kind in MustReachPhase::ALL {
let serialized = serde_json::to_string(&kind)
.expect("MustReachPhase serializes")
.trim_matches('"')
.to_string();
assert_eq!(
kind.as_str(),
serialized,
"as_str() must match serde output for {kind:?}",
);
}
}
#[test]
fn must_reach_phase_as_str_matches_process_phase_as_str() {
for kind in MustReachPhase::ALL {
assert_eq!(
kind.as_str(),
kind.as_process_phase().as_str(),
"MustReachPhase::as_str() and ProcessPhase::as_str() drift for {kind:?}",
);
}
}
#[test]
fn must_reach_phase_display_matches_as_str() {
for kind in MustReachPhase::ALL {
assert_eq!(kind.to_string(), kind.as_str());
}
}
#[test]
fn unknown_must_reach_phase_errors() {
use std::str::FromStr;
for bad in [
"running", "ATTESTED", "Atested", "Pending", "Failed", "Reaped",
] {
let err = MustReachPhase::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn must_reach_phase_from_delegates_to_as_process_phase() {
for kind in MustReachPhase::ALL {
let via_from: ProcessPhase = kind.into();
assert_eq!(
via_from,
kind.as_process_phase(),
"From<MustReachPhase> drift for {kind:?}",
);
}
}
#[test]
fn must_reach_phase_projects_only_to_live_checkpoints() {
for kind in MustReachPhase::ALL {
let p = kind.as_process_phase();
assert!(
p.is_running(),
"{kind:?} → {p:?} must be a live checkpoint (Running or Attested)",
);
}
}
#[test]
fn must_reach_phase_projection_is_injective() {
let mut seen = std::collections::HashSet::new();
for kind in MustReachPhase::ALL {
let p = kind.as_process_phase();
assert!(
seen.insert(p),
"MustReachPhase projection collision: {kind:?} → {p:?}",
);
}
assert_eq!(seen.len(), MustReachPhase::ALL.len());
}
}