use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct Notification {
pub id: String,
pub priority: NotificationPriority,
#[serde(default)]
pub require_ack: bool,
pub title: String,
pub body: String,
pub issued_at: chrono::DateTime<chrono::Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acked_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum NotificationPriority {
Info,
Warn,
Emergency,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsListParams {
#[serde(default)]
pub filter: NotificationsFilter,
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl Default for NotificationsListParams {
fn default() -> Self {
Self {
filter: NotificationsFilter::default(),
limit: default_limit(),
cursor: None,
}
}
}
fn default_limit() -> u32 {
50
}
#[derive(
Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum NotificationsFilter {
#[default]
Unread,
All,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsListResult {
pub items: Vec<Notification>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct NotificationsSubscribeParams {}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsSubscribeResult {
pub subscription: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsUnsubscribeParams {
pub subscription: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationNewParams {
#[serde(flatten)]
pub notification: Notification,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsAckParams {
pub id: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct NotificationsAckResult {
pub acked_at: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn priority_serialises_snake_case() {
for (variant, expected) in [
(NotificationPriority::Info, "\"info\""),
(NotificationPriority::Warn, "\"warn\""),
(NotificationPriority::Emergency, "\"emergency\""),
] {
let s = serde_json::to_string(&variant).unwrap();
assert_eq!(s, expected, "encode {variant:?}");
let back: NotificationPriority = serde_json::from_str(expected).unwrap();
assert_eq!(back, variant, "round-trip {expected}");
}
}
#[test]
fn filter_defaults_to_unread() {
let p = NotificationsListParams::default();
assert_eq!(p.filter, NotificationsFilter::Unread);
let p: NotificationsListParams = serde_json::from_str("{}").unwrap();
assert_eq!(p.filter, NotificationsFilter::Unread);
assert_eq!(p.limit, 50);
}
#[test]
fn notification_new_spec_example_decodes() {
let wire = r#"{
"id":"notif-9f3a","priority":"emergency","require_ack":true,
"title":"緊急: ネットワーク機器メンテ","body":"22時から30分停止します",
"issued_at":"2026-05-20T12:00:00Z","issued_by":"infra-team"
}"#;
let p: NotificationNewParams = serde_json::from_str(wire).expect("decode");
assert_eq!(p.notification.id, "notif-9f3a");
assert_eq!(p.notification.priority, NotificationPriority::Emergency);
assert!(p.notification.require_ack);
assert_eq!(p.notification.title, "緊急: ネットワーク機器メンテ");
assert_eq!(p.notification.issued_by.as_deref(), Some("infra-team"));
}
#[test]
fn ack_result_round_trips() {
let t = chrono::Utc.with_ymd_and_hms(2026, 5, 20, 12, 0, 5).unwrap();
let r = NotificationsAckResult { acked_at: t };
let json = serde_json::to_string(&r).unwrap();
let back: NotificationsAckResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.acked_at, t);
}
#[test]
fn notifications_list_paginates_via_cursor() {
let p = NotificationsListParams {
filter: NotificationsFilter::All,
limit: 25,
cursor: None,
};
let v = serde_json::to_value(&p).unwrap();
assert!(v.get("cursor").is_none(), "wire: {v:?}");
let p = NotificationsListParams {
cursor: Some("opaque-token".into()),
..NotificationsListParams::default()
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["cursor"], "opaque-token");
}
}