use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum IssueStatus {
Open,
Closed,
}
impl IssueStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Open => "open",
Self::Closed => "closed",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum NotificationStatus {
Pending,
Sent,
Failed,
Unknown,
NotAttempted,
}
impl NotificationStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Sent => "sent",
Self::Failed => "failed",
Self::Unknown => "unknown",
Self::NotAttempted => "not_attempted",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Notification {
pub status: NotificationStatus,
pub error: Option<String>,
}
impl Notification {
pub fn sent() -> Self {
Self {
status: NotificationStatus::Sent,
error: None,
}
}
pub fn failed(error: &str) -> Self {
Self {
status: NotificationStatus::Failed,
error: Some(error.to_owned()),
}
}
pub fn not_attempted() -> Self {
Self {
status: NotificationStatus::NotAttempted,
error: None,
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct OpenIssue {
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 200))]
pub id: String,
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 250))]
pub title: Option<String>,
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 1024))]
pub message: Option<String>,
#[serde(default)]
pub severity: Severity,
pub remind_every_seconds: Option<u64>,
#[serde(default)]
pub notify_on_resolution: bool,
}
pub fn validate_id(id: &str) -> Result<(), &'static str> {
if id.is_empty() || id.chars().count() > 200 {
Err("id must contain between 1 and 200 characters")
} else {
Ok(())
}
}
impl OpenIssue {
pub fn validate(&self) -> Result<(), &'static str> {
validate_id(&self.id)?;
validate_interval(self.remind_every_seconds)?;
if self
.title
.as_ref()
.is_some_and(|s| s.is_empty() || s.chars().count() > 250)
{
return Err("title must contain between 1 and 250 characters");
}
if self
.message
.as_ref()
.is_some_and(|s| s.is_empty() || s.chars().count() > 1024)
{
return Err("message must contain between 1 and 1024 characters");
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct CloseIssue {
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 200))]
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Issue {
pub severity: Severity,
pub remind_every_seconds: Option<u64>,
pub notify_on_resolution: bool,
pub delivery_id: Option<i64>,
pub id: String,
pub status: IssueStatus,
pub title: String,
pub message: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub opened_at: DateTime<Utc>,
pub closed_at: Option<DateTime<Utc>>,
pub opening_count: i64,
pub notification: Notification,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct MutationResult {
pub delivery_id: Option<i64>,
pub issue: Issue,
pub changed: bool,
pub notification: Notification,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Alert {
#[serde(default)]
pub severity: Severity,
pub group_key: Option<String>,
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 250))]
pub title: String,
#[cfg_attr(feature = "openapi", schema(min_length = 1, max_length = 1024))]
pub message: String,
}
impl Alert {
pub fn validate(&self) -> Result<(), &'static str> {
if let Some(key) = &self.group_key {
validate_id(key)?;
}
if self.title.is_empty() || self.title.chars().count() > 250 {
return Err("title must contain between 1 and 250 characters");
}
if self.message.is_empty() || self.message.chars().count() > 1024 {
return Err("message must contain between 1 and 1024 characters");
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AlertResult {
pub delivery_id: i64,
pub notification: Notification,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct IssueList {
pub items: Vec<Issue>,
pub total: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
#[serde(deny_unknown_fields)]
pub struct ListQuery {
pub status: Option<IssueStatus>,
#[serde(default = "default_limit")]
#[cfg_attr(feature = "openapi", param(minimum = 1, maximum = 1000, default = 100))]
pub limit: u32,
#[serde(default)]
pub offset: u32,
}
fn default_limit() -> u32 {
100
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ErrorBody {
pub detail: String,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Info,
#[default]
Warning,
Critical,
}
pub fn validate_interval(value: Option<u64>) -> Result<(), &'static str> {
if value.is_some_and(|value| !(1..=31_536_000).contains(&value)) {
return Err("interval must be between 1 and 31536000 seconds");
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct HeartbeatInput {
pub id: String,
pub title: String,
pub interval_seconds: u64,
#[serde(default)]
pub grace_seconds: u64,
#[serde(default)]
pub severity: Severity,
#[serde(default)]
pub notify_on_recovery: bool,
}
impl HeartbeatInput {
pub fn validate(&self) -> Result<(), &'static str> {
validate_id(&self.id)?;
if self.title.is_empty() || self.title.chars().count() > 250 {
return Err("title must contain between 1 and 250 characters");
}
validate_interval(Some(self.interval_seconds))?;
if self.grace_seconds > 31_536_000 {
return Err("grace_seconds must be at most 31536000");
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Heartbeat {
#[serde(flatten)]
pub config: HeartbeatInput,
pub last_seen: i64,
pub due_at: i64,
pub overdue: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DestinationOutcome {
pub destination: String,
pub attempts: u32,
pub notification: Notification,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Delivery {
pub id: i64,
pub title: String,
pub message: String,
pub severity: Severity,
pub kind: String,
pub count: u64,
pub created_at: i64,
pub next_attempt_at: i64,
pub notification: Notification,
pub destinations: Vec<DestinationOutcome>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Health {
pub status: String,
}