forge_primitives/
gotify.rs1use serde::{Deserialize, Serialize};
12
13#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct GotifyConfig {
20 pub url: String,
21 pub client_token: String,
24}
25
26impl std::fmt::Debug for GotifyConfig {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("GotifyConfig")
31 .field("url", &self.url)
32 .field("client_token", &"[redacted]")
33 .finish()
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct GotifyMessage {
40 pub id: u64,
41 pub appid: u64,
43 pub title: String,
44 pub message: String,
45 pub priority: u8,
46 pub date: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(from = "GotifySubscriptionRepr")]
56pub struct GotifySubscription {
57 pub id: uuid::Uuid,
58 pub project: String,
59 pub team_role: Option<String>,
64 pub applications: Vec<String>,
67 pub min_priority: Option<u8>,
69 pub created_at: std::time::SystemTime,
70}
71
72#[derive(Deserialize)]
78struct GotifySubscriptionRepr {
79 id: uuid::Uuid,
80 project: String,
81 team_role: Option<String>,
82 #[serde(default)]
83 applications: Vec<String>,
84 #[serde(default)]
85 application: Option<String>,
86 min_priority: Option<u8>,
87 created_at: std::time::SystemTime,
88}
89
90impl From<GotifySubscriptionRepr> for GotifySubscription {
91 fn from(repr: GotifySubscriptionRepr) -> Self {
92 let mut applications = repr.applications;
93 if applications.is_empty() {
94 applications.extend(repr.application);
95 }
96 Self {
97 id: repr.id,
98 project: repr.project,
99 team_role: repr.team_role,
100 applications,
101 min_priority: repr.min_priority,
102 created_at: repr.created_at,
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::time::{Duration, SystemTime};
111
112 #[test]
113 fn gotify_types_round_trip() {
114 let sub = GotifySubscription {
115 id: uuid::Uuid::from_u128(0x1234_5678_9abc_def0_1234_5678_9abc_def0),
116 project: "web-api".to_owned(),
117 team_role: Some("analyst".to_owned()),
118 applications: vec!["alerts".to_owned()],
119 min_priority: Some(5),
120 created_at: SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000),
121 };
122 let json = serde_json::to_string(&sub).expect("serialize");
123 let back: GotifySubscription = serde_json::from_str(&json).expect("deserialize");
124 assert_eq!(sub, back);
125
126 let line = r#"{"id":1,"appid":3,"title":"t","message":"m","priority":5,"date":"2026-07-03T09:18:00Z"}"#;
127 let msg: GotifyMessage = serde_json::from_str(line).expect("deserialize stream line");
128 assert_eq!(msg.appid, 3);
129 assert_eq!(msg.priority, 5);
130 }
131
132 #[test]
133 fn legacy_v0_20_0_record_migrates_forward() {
134 let base = GotifySubscription {
138 id: uuid::Uuid::from_u128(1),
139 project: "p".to_owned(),
140 team_role: None,
141 applications: vec![],
142 min_priority: Some(5),
143 created_at: SystemTime::UNIX_EPOCH,
144 };
145 let mut legacy = serde_json::to_value(&base).expect("serialize");
146 legacy.as_object_mut().expect("object").remove("applications");
147
148 legacy.as_object_mut().expect("object").insert("application".to_owned(), "alerts".into());
149 let named: GotifySubscription =
150 serde_json::from_value(legacy.clone()).expect("legacy named record");
151 assert_eq!(
152 named.applications,
153 vec!["alerts".to_owned()],
154 "a real old single-app filter folds forward",
155 );
156
157 legacy
158 .as_object_mut()
159 .expect("object")
160 .insert("application".to_owned(), serde_json::Value::Null);
161 let any: GotifySubscription =
162 serde_json::from_value(legacy).expect("legacy null-application record");
163 assert!(any.applications.is_empty(), "a null application becomes the match-any set");
164 }
165}