1use serde::{Deserialize, Serialize};
2
3pub type Result<T> = std::result::Result<T, Error>;
5
6const API_URL: &str = "https://uptime.betterstack.com/api";
7
8#[derive(thiserror::Error, Debug)]
10pub enum Error {
11 #[error("reqwest: {0}")]
12 Reqwest(#[from] reqwest::Error),
13
14 #[error("Uptime API: {0}")]
15 UptimeApi(String),
16
17 #[error("serde_json: {0}")]
18 SerdeJson(#[from] serde_json::Error),
19}
20
21fn maybe_uptime_api_error<T>(value: serde_json::Value) -> Result<T>
22where
23 T: serde::de::DeserializeOwned,
24{
25 #[derive(Deserialize)]
26 struct ErrorResponse {
27 error: String,
28 }
29 if let Ok(ErrorResponse { error }) = serde_json::from_value::<ErrorResponse>(value.clone()) {
30 Err(Error::UptimeApi(error))
31 } else {
32 serde_json::from_value(value).map_err(|err| err.into())
33 }
34}
35
36pub struct Uptime {
37 pub token: String,
38}
39
40#[derive(Debug, Serialize, Default)]
41pub struct IncidentRequest {
42 pub requester_email: String,
43 pub name: String,
44 pub summary: String,
45 pub description: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub call: Option<bool>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub sms: Option<bool>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub email: Option<bool>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub push: Option<bool>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub team_wait: Option<u64>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub poliy_id: Option<String>,
58}
59
60#[derive(Clone, Debug, Deserialize)]
61pub struct Incident {
62 pub id: String,
63}
64
65impl Uptime {
66 pub async fn heartbeat(&self, identifier: String) -> Result<()> {
68 let url = format!("{API_URL}/v1/heartbeat/{identifier}");
69 match reqwest::get(url).await?.error_for_status() {
70 Ok(_) => Ok(()),
71 Err(err) => Err(err.into()),
72 }
73 }
74
75 pub async fn create_incident(&self, request: IncidentRequest) -> Result<Incident> {
77 let url = format!("{API_URL}/v2/incidents");
78
79 #[derive(Debug, Deserialize)]
80 struct IncidentResponse {
81 data: Incident,
82 }
83
84 let response = maybe_uptime_api_error::<IncidentResponse>(
85 reqwest::Client::builder()
86 .build()?
87 .post(url)
88 .header("Content-Type", "application/json")
89 .header("Authorization", format!("Bearer {}", self.token))
90 .json(&request)
91 .send()
92 .await?
93 .error_for_status()?
94 .json()
95 .await?,
96 )?;
97
98 Ok(response.data)
99 }
100}