Skip to main content

flares_types/
lib.rs

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    /// The attempt made by this request, not the latest historical outcome.
156    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}