use std::time::Duration;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HealthCheckSpec {
#[serde(with = "duration_secs")]
pub timeout: Duration,
pub max_retries: u32,
#[serde(with = "duration_secs")]
pub retry_interval: Duration,
}
impl HealthCheckSpec {
#[must_use]
pub const fn new(timeout: Duration, max_retries: u32, retry_interval: Duration) -> Self {
Self {
timeout,
max_retries,
retry_interval,
}
}
}
mod duration_secs {
use serde::{Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(
d: &Duration,
s: S,
) -> std::result::Result<S::Ok, S::Error> {
s.serialize_f64(d.as_secs_f64())
}
pub fn deserialize<'de, D: Deserializer<'de>>(
d: D,
) -> std::result::Result<Duration, D::Error> {
use serde::Deserialize;
let secs = f64::deserialize(d)?;
if !secs.is_finite() || secs < 0.0 {
return Err(serde::de::Error::custom(
"duration seconds must be a finite non-negative f64",
));
}
Ok(Duration::from_secs_f64(secs))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationalState {
Inactive,
Provisioning,
Starting,
HealthCheck,
Ready,
Running,
Stopping,
Stopped,
Failed,
Terminated,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LiveStatusSummary {
pub state: OperationalState,
pub summary: String,
}
impl LiveStatusSummary {
#[must_use]
pub fn unknown(summary: impl Into<String>) -> Self {
Self {
state: OperationalState::Unknown,
summary: summary.into(),
}
}
#[must_use]
pub fn inactive() -> Self {
Self {
state: OperationalState::Inactive,
summary: "element not yet started".to_owned(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShutdownSemantics {
#[default]
Service,
Command,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StateTransition {
pub from: OperationalState,
pub to: OperationalState,
pub summary: String,
pub timestamp: Timestamp,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shutdown_default_is_service() {
assert_eq!(ShutdownSemantics::default(), ShutdownSemantics::Service);
}
#[test]
fn live_status_helpers() {
let u = LiveStatusSummary::unknown("probe failed");
assert_eq!(u.state, OperationalState::Unknown);
assert_eq!(u.summary, "probe failed");
let i = LiveStatusSummary::inactive();
assert_eq!(i.state, OperationalState::Inactive);
}
#[test]
fn operational_state_serde_is_snake_case() {
let s = serde_json::to_string(&OperationalState::HealthCheck).unwrap();
assert_eq!(s, "\"health_check\"");
let back: OperationalState = serde_json::from_str(&s).unwrap();
assert_eq!(back, OperationalState::HealthCheck);
}
#[test]
fn health_check_spec_serde_roundtrip() {
let spec = HealthCheckSpec::new(
Duration::from_secs(30),
5,
Duration::from_millis(500),
);
let json = serde_json::to_string(&spec).unwrap();
let back: HealthCheckSpec = serde_json::from_str(&json).unwrap();
assert_eq!(spec, back);
}
#[test]
fn state_transition_serde_roundtrip() {
let t = StateTransition {
from: OperationalState::Unknown,
to: OperationalState::Ready,
summary: "probe passed".to_owned(),
timestamp: Timestamp::from_second(0).unwrap(),
};
let json = serde_json::to_string(&t).unwrap();
let back: StateTransition = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
}