1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
6#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
7#[serde(rename_all = "snake_case")]
8pub enum IssueStatus {
9 Open,
10 Closed,
11}
12
13impl IssueStatus {
14 pub fn as_str(self) -> &'static str {
15 match self {
16 Self::Open => "open",
17 Self::Closed => "closed",
18 }
19 }
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24#[serde(rename_all = "snake_case")]
25pub enum NotificationStatus {
26 Pending,
27 Sent,
28 Failed,
29 Unknown,
30 NotAttempted,
31}
32
33impl NotificationStatus {
34 pub fn as_str(self) -> &'static str {
35 match self {
36 Self::Pending => "pending",
37 Self::Sent => "sent",
38 Self::Failed => "failed",
39 Self::Unknown => "unknown",
40 Self::NotAttempted => "not_attempted",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
47pub struct Notification {
48 pub status: NotificationStatus,
49 pub error: Option<String>,
50}
51
52impl Notification {
53 pub fn sent() -> Self {
54 Self {
55 status: NotificationStatus::Sent,
56 error: None,
57 }
58 }
59
60 pub fn failed(error: &str) -> Self {
61 Self {
62 status: NotificationStatus::Failed,
63 error: Some(error.to_owned()),
64 }
65 }
66
67 pub fn not_attempted() -> Self {
68 Self {
69 status: NotificationStatus::NotAttempted,
70 error: None,
71 }
72 }
73}
74
75#[derive(Debug, Default, Clone, Serialize, Deserialize)]
76#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
77#[serde(deny_unknown_fields)]
78pub struct OpenIssue {
79 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 200))]
80 pub id: String,
81 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 250))]
82 pub title: Option<String>,
83 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 1024))]
84 pub message: Option<String>,
85 #[serde(default)]
86 pub severity: Severity,
87 pub remind_every_seconds: Option<u64>,
88 #[serde(default)]
89 pub notify_on_resolution: bool,
90}
91
92pub fn validate_id(id: &str) -> Result<(), &'static str> {
93 if id.is_empty() || id.chars().count() > 200 {
94 Err("id must contain between 1 and 200 characters")
95 } else {
96 Ok(())
97 }
98}
99
100impl OpenIssue {
101 pub fn validate(&self) -> Result<(), &'static str> {
102 validate_id(&self.id)?;
103 validate_interval(self.remind_every_seconds)?;
104 if self
105 .title
106 .as_ref()
107 .is_some_and(|s| s.is_empty() || s.chars().count() > 250)
108 {
109 return Err("title must contain between 1 and 250 characters");
110 }
111 if self
112 .message
113 .as_ref()
114 .is_some_and(|s| s.is_empty() || s.chars().count() > 1024)
115 {
116 return Err("message must contain between 1 and 1024 characters");
117 }
118 Ok(())
119 }
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
124#[serde(deny_unknown_fields)]
125pub struct CloseIssue {
126 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 200))]
127 pub id: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
132pub struct Issue {
133 pub severity: Severity,
134 pub remind_every_seconds: Option<u64>,
135 pub notify_on_resolution: bool,
136 pub delivery_id: Option<i64>,
137 pub id: String,
138 pub status: IssueStatus,
139 pub title: String,
140 pub message: String,
141 pub created_at: DateTime<Utc>,
142 pub updated_at: DateTime<Utc>,
143 pub opened_at: DateTime<Utc>,
144 pub closed_at: Option<DateTime<Utc>>,
145 pub opening_count: i64,
146 pub notification: Notification,
147}
148
149#[derive(Debug, Serialize, Deserialize)]
150#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
151pub struct MutationResult {
152 pub delivery_id: Option<i64>,
153 pub issue: Issue,
154 pub changed: bool,
155 pub notification: Notification,
157}
158
159#[derive(Debug, Default, Clone, Serialize, Deserialize)]
160#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
161#[serde(deny_unknown_fields)]
162pub struct Alert {
163 #[serde(default)]
164 pub severity: Severity,
165 pub group_key: Option<String>,
166 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 250))]
167 pub title: String,
168 #[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 1024))]
169 pub message: String,
170}
171
172impl Alert {
173 pub fn validate(&self) -> Result<(), &'static str> {
174 if let Some(key) = &self.group_key {
175 validate_id(key)?;
176 }
177 if self.title.is_empty() || self.title.chars().count() > 250 {
178 return Err("title must contain between 1 and 250 characters");
179 }
180 if self.message.is_empty() || self.message.chars().count() > 1024 {
181 return Err("message must contain between 1 and 1024 characters");
182 }
183 Ok(())
184 }
185}
186
187#[derive(Debug, Serialize, Deserialize)]
188#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
189pub struct AlertResult {
190 pub delivery_id: i64,
191 pub notification: Notification,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
196pub struct IssueList {
197 pub items: Vec<Issue>,
198 pub total: u64,
199 pub limit: u32,
200 pub offset: u32,
201}
202
203#[derive(Debug, Deserialize)]
204#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
205#[serde(deny_unknown_fields)]
206pub struct ListQuery {
207 pub status: Option<IssueStatus>,
208 #[serde(default = "default_limit")]
209 #[cfg_attr(feature = "openapi", param(minimum = 1, maximum = 1000, default = 100))]
210 pub limit: u32,
211 #[serde(default)]
212 pub offset: u32,
213}
214
215fn default_limit() -> u32 {
216 100
217}
218
219#[derive(Debug, Serialize, Deserialize)]
220#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
221pub struct ErrorBody {
222 pub detail: String,
223}
224
225#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
226#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
227#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
228#[serde(rename_all = "snake_case")]
229pub enum Severity {
230 Info,
231 #[default]
232 Warning,
233 Critical,
234}
235
236pub fn validate_interval(value: Option<u64>) -> Result<(), &'static str> {
237 if value.is_some_and(|value| !(1..=31_536_000).contains(&value)) {
238 return Err("interval must be between 1 and 31536000 seconds");
239 }
240 Ok(())
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
245#[serde(deny_unknown_fields)]
246pub struct HeartbeatInput {
247 pub id: String,
248 pub title: String,
249 pub interval_seconds: u64,
250 #[serde(default)]
251 pub grace_seconds: u64,
252 #[serde(default)]
253 pub severity: Severity,
254 #[serde(default)]
255 pub notify_on_recovery: bool,
256}
257impl HeartbeatInput {
258 pub fn validate(&self) -> Result<(), &'static str> {
259 validate_id(&self.id)?;
260 if self.title.is_empty() || self.title.chars().count() > 250 {
261 return Err("title must contain between 1 and 250 characters");
262 }
263 validate_interval(Some(self.interval_seconds))?;
264 if self.grace_seconds > 31_536_000 {
265 return Err("grace_seconds must be at most 31536000");
266 }
267 Ok(())
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
273pub struct Heartbeat {
274 #[serde(flatten)]
275 pub config: HeartbeatInput,
276 pub last_seen: i64,
277 pub due_at: i64,
278 pub overdue: bool,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
283pub struct DestinationOutcome {
284 pub destination: String,
285 pub attempts: u32,
286 pub notification: Notification,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
291pub struct Delivery {
292 pub id: i64,
293 pub title: String,
294 pub message: String,
295 pub severity: Severity,
296 pub kind: String,
297 pub count: u64,
298 pub created_at: i64,
299 pub next_attempt_at: i64,
300 pub notification: Notification,
301 pub destinations: Vec<DestinationOutcome>,
302}
303
304#[derive(Debug, Serialize, Deserialize)]
305pub struct Health {
306 pub status: String,
307}