use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct MaintenanceListParams {
#[serde(default = "default_window_days")]
pub window_days: u32,
}
impl Default for MaintenanceListParams {
fn default() -> Self {
Self {
window_days: default_window_days(),
}
}
}
fn default_window_days() -> u32 {
7
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct MaintenanceListResult {
pub items: Vec<MaintenanceItem>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct MaintenanceItem {
pub schedule_id: String,
pub manifest_id: String,
pub display_name: String,
pub next_fire_at: chrono::DateTime<chrono::Utc>,
#[serde(default)]
pub deferrable: bool,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct MaintenanceDeferParams {
pub schedule_id: String,
pub duration: DeferDuration,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DeferDuration {
#[serde(rename = "15m")]
M15,
#[serde(rename = "30m")]
M30,
#[serde(rename = "1h")]
H1,
}
impl DeferDuration {
pub fn as_duration(self) -> chrono::Duration {
match self {
Self::M15 => chrono::Duration::minutes(15),
Self::M30 => chrono::Duration::minutes(30),
Self::H1 => chrono::Duration::hours(1),
}
}
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct MaintenanceDeferResult {
pub new_fire_at: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn params_default_window_is_seven_days() {
let p = MaintenanceListParams::default();
assert_eq!(p.window_days, 7);
let p: MaintenanceListParams = serde_json::from_str("{}").unwrap();
assert_eq!(p.window_days, 7);
}
#[test]
fn item_with_deferrable_round_trips() {
let t = chrono::Utc
.with_ymd_and_hms(2026, 5, 25, 14, 30, 0)
.unwrap();
let i = MaintenanceItem {
schedule_id: "weekly-reboot".into(),
manifest_id: "reboot".into(),
display_name: "再起動".into(),
next_fire_at: t,
deferrable: true,
};
let json = serde_json::to_string(&i).unwrap();
let back: MaintenanceItem = serde_json::from_str(&json).unwrap();
assert_eq!(back.schedule_id, i.schedule_id);
assert_eq!(back.display_name, "再起動");
assert_eq!(back.next_fire_at, t);
assert!(back.deferrable);
}
#[test]
fn defer_duration_wire_matches_spec_2_1_humantime() {
for (variant, expected) in [
(DeferDuration::M15, "\"15m\""),
(DeferDuration::M30, "\"30m\""),
(DeferDuration::H1, "\"1h\""),
] {
let s = serde_json::to_string(&variant).unwrap();
assert_eq!(s, expected, "encode {variant:?}");
let back: DeferDuration = serde_json::from_str(expected).unwrap();
assert_eq!(back, variant, "round-trip {expected}");
}
}
#[test]
fn defer_duration_as_duration_matches_spec_table() {
assert_eq!(
DeferDuration::M15.as_duration(),
chrono::Duration::minutes(15)
);
assert_eq!(
DeferDuration::M30.as_duration(),
chrono::Duration::minutes(30)
);
assert_eq!(DeferDuration::H1.as_duration(), chrono::Duration::hours(1));
}
#[test]
fn defer_result_round_trips() {
let t = chrono::Utc.with_ymd_and_hms(2026, 5, 24, 15, 0, 0).unwrap();
let r = MaintenanceDeferResult { new_fire_at: t };
let json = serde_json::to_string(&r).unwrap();
let back: MaintenanceDeferResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.new_fire_at, t);
}
#[test]
fn item_deferrable_defaults_to_false() {
let wire = r#"{
"schedule_id":"x","manifest_id":"y","display_name":"z",
"next_fire_at":"2026-05-24T00:00:00Z"
}"#;
let i: MaintenanceItem = serde_json::from_str(wire).unwrap();
assert!(!i.deferrable);
}
}