#![deny(missing_docs)]
#[cfg(test)]
mod tests;
pub mod types;
use anyhow::Result;
use reqwest::{Method, Request, StatusCode, Url};
use serde::{Deserialize, Serialize};
use crate::types::{EscalationPolicy, EscalationPolicyListResponse, Service, ServiceListResponse, ServiceObject};
#[derive(Debug, Clone)]
pub struct Client {
client: reqwest_middleware::ClientWithMiddleware,
token: String,
}
pub const DEFAULT_HOST: &str = "https://api.pagerduty.com";
pub const LIMIT: i64 = 30;
impl Client {
#[tracing::instrument]
pub fn new<T>(token: T) -> Self
where
T: ToString + std::fmt::Debug,
{
let http = reqwest::Client::builder().build();
match http {
Ok(c) => {
let retry_policy = reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
let client = reqwest_middleware::ClientBuilder::new(c)
.with(reqwest_tracing::TracingMiddleware::default())
.with(reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
Self {
client,
token: token.to_string(),
}
}
Err(e) => panic!("creating client failed: {:?}", e),
}
}
pub fn new_from_env() -> Self {
let token = std::env::var("PAGERDUTY_TOKEN").expect("must set PAGERDUTY_TOKEN");
Client::new(token)
}
#[tracing::instrument(skip(self, body))]
fn request<P, B>(&self, method: Method, path: P, body: &B, query: Option<Vec<(&str, &str)>>) -> Result<Request>
where
P: ToString + std::fmt::Debug,
B: serde::Serialize,
{
let url = Url::parse(&format!(
"{}/{}",
DEFAULT_HOST,
path.to_string().trim_start_matches('/')
))?;
let mut rb = self.client.request(method.clone(), url);
rb = rb.header("Authorization", &format!("Token token={}", self.token));
rb = rb.header("User-Agent", "kittycad/pagerduty-rust-api");
match query {
None => (),
Some(val) => {
rb = rb.query(&val);
}
}
if method != Method::GET && method != Method::DELETE {
rb = rb.json(body);
}
Ok(rb.build()?)
}
#[tracing::instrument(skip(self))]
pub async fn create_service(&self, service: &Service) -> Result<Service> {
let request = self.request(
Method::POST,
"/services",
&ServiceObject {
service: service.clone(),
},
None,
)?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
StatusCode::CREATED => (),
StatusCode::ACCEPTED => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
let text = resp.text().await?;
let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
Error::Json {
body: text,
message: err.to_string(),
}
})?;
Ok(data.service)
}
#[tracing::instrument(skip(self))]
pub async fn list_services(&self) -> Result<Vec<Service>> {
let mut services: Vec<Service> = Default::default();
let mut resp = self.list_services_internal(0).await?;
services.append(&mut resp.services);
while resp.more {
let offset = resp.offset + LIMIT;
resp = self.list_services_internal(offset).await?;
services.append(&mut resp.services);
}
Ok(services)
}
#[tracing::instrument(skip(self))]
async fn list_services_internal(&self, offset: i64) -> Result<ServiceListResponse> {
let limit_str = format!("{}", LIMIT);
let mut query: Vec<(&str, &str)> = vec![("limit", &limit_str)];
let offset_str = format!("{}", offset);
if offset > 0 {
query.push(("offset", &offset_str));
}
let request = self.request(Method::GET, "/services", &(), Some(query))?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
let text = resp.text().await?;
let data: ServiceListResponse = serde_json::from_str(&text).map_err(|err| {
Error::Json {
body: text,
message: err.to_string(),
}
})?;
Ok(data)
}
#[tracing::instrument(skip(self))]
pub async fn list_escalation_policies(&self) -> Result<Vec<EscalationPolicy>> {
let mut escalation_policies: Vec<EscalationPolicy> = Default::default();
let mut resp = self.list_escalation_policies_internal(0).await?;
escalation_policies.append(&mut resp.escalation_policies);
while resp.more {
let offset = resp.offset + LIMIT;
resp = self.list_escalation_policies_internal(offset).await?;
escalation_policies.append(&mut resp.escalation_policies);
}
Ok(escalation_policies)
}
#[tracing::instrument(skip(self))]
async fn list_escalation_policies_internal(&self, offset: i64) -> Result<EscalationPolicyListResponse> {
let limit_str = format!("{}", LIMIT);
let mut query: Vec<(&str, &str)> = vec![("limit", &limit_str)];
let offset_str = format!("{}", offset);
if offset > 0 {
query.push(("offset", &offset_str));
}
let request = self.request(Method::GET, "/escalation_policies", &(), Some(query))?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
let text = resp.text().await?;
let data: EscalationPolicyListResponse = serde_json::from_str(&text).map_err(|err| {
Error::Json {
body: text,
message: err.to_string(),
}
})?;
Ok(data)
}
#[tracing::instrument(skip(self))]
pub async fn get_service(&self, id: &str) -> Result<Service> {
let request = self.request(Method::GET, &format!("/services/{}", id), &(), None)?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
let text = resp.text().await?;
let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
Error::Json {
body: text,
message: err.to_string(),
}
})?;
Ok(data.service)
}
#[tracing::instrument(skip(self))]
pub async fn update_service(&self, service: &Service) -> Result<Service> {
let request = self.request(
Method::PUT,
&format!("/services/{}", service.id),
&ServiceObject {
service: service.clone(),
},
None,
)?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
StatusCode::CREATED => (),
StatusCode::ACCEPTED => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
let text = resp.text().await?;
let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
Error::Json {
body: text,
message: err.to_string(),
}
})?;
Ok(data.service)
}
#[tracing::instrument(skip(self))]
pub async fn delete_service(&self, id: &str) -> Result<()> {
let request = self.request(Method::DELETE, &format!("/services/{}", id), &(), None)?;
let resp = self.client.execute(request).await?;
match resp.status() {
StatusCode::OK => (),
StatusCode::CREATED => (),
StatusCode::ACCEPTED => (),
StatusCode::NO_CONTENT => (),
s => {
let body = resp.text().await?;
let err: Error = match serde_json::from_str(&body) {
Ok(j) => j,
Err(_) => {
Error::Http {
status: s.to_string(),
code: s.as_u16(),
message: body,
}
}
};
return Err(err.into());
}
};
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[serde(untagged)]
pub enum Error {
#[error("{status} {code}: {message}")]
Http {
#[serde(rename = "error_code")]
status: String,
#[serde(default)]
code: u16,
message: String,
},
#[error("{error}: {description} {error_uri}")]
Pagerduty {
error: String,
description: String,
error_uri: String,
},
#[error("{message}: {body}")]
Json {
body: String,
message: String,
},
}