pagerduty_api/
lib.rs

1//! A library for working with the Pagerduty API.
2//! This library is an implementation of: <https://developer.pagerduty.com/api-reference/>
3#![deny(missing_docs)]
4
5#[cfg(test)]
6mod tests;
7pub mod types;
8
9use anyhow::Result;
10use reqwest::{Method, Request, StatusCode, Url};
11use serde::{Deserialize, Serialize};
12
13use crate::types::{EscalationPolicy, EscalationPolicyListResponse, Service, ServiceListResponse, ServiceObject};
14
15/// Entrypoint for interacting with the Pagerduty API.
16#[derive(Debug, Clone)]
17pub struct Client {
18    client: reqwest_middleware::ClientWithMiddleware,
19    token: String,
20}
21
22/// The default host for the API.
23pub const DEFAULT_HOST: &str = "https://api.pagerduty.com";
24/// The limit for paginating.
25pub const LIMIT: i64 = 30;
26
27impl Client {
28    /// Create a new Pagerduty client struct.
29    #[tracing::instrument]
30    pub fn new<T>(token: T) -> Self
31    where
32        T: ToString + std::fmt::Debug,
33    {
34        let http = reqwest::Client::builder().build();
35        match http {
36            Ok(c) => {
37                let retry_policy = reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
38                let client = reqwest_middleware::ClientBuilder::new(c)
39                    // Trace HTTP requests. See the tracing crate to make use of these traces.
40                    .with(reqwest_tracing::TracingMiddleware::default())
41                    // Retry failed requests.
42                    .with(reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy))
43                    .build();
44
45                Self {
46                    client,
47                    token: token.to_string(),
48                }
49            }
50            Err(e) => panic!("creating client failed: {:?}", e),
51        }
52    }
53
54    /// Create a new Client struct from environment variables. As long as the function is
55    /// given a valid API key and your requests will work.
56    pub fn new_from_env() -> Self {
57        let token = std::env::var("PAGERDUTY_TOKEN").expect("must set PAGERDUTY_TOKEN");
58
59        Client::new(token)
60    }
61
62    #[tracing::instrument(skip(self, body))]
63    fn request<P, B>(&self, method: Method, path: P, body: &B, query: Option<Vec<(&str, &str)>>) -> Result<Request>
64    where
65        P: ToString + std::fmt::Debug,
66        B: serde::Serialize,
67    {
68        let url = Url::parse(&format!(
69            "{}/{}",
70            DEFAULT_HOST,
71            path.to_string().trim_start_matches('/')
72        ))?;
73
74        let mut rb = self.client.request(method.clone(), url);
75
76        // Add our token to the request.
77        rb = rb.header("Authorization", &format!("Token token={}", self.token));
78
79        // Add our user agent.
80        rb = rb.header("User-Agent", "kittycad/pagerduty-rust-api");
81
82        match query {
83            None => (),
84            Some(val) => {
85                rb = rb.query(&val);
86            }
87        }
88
89        // Add the body, this is to ensure our GET and DELETE calls succeed.
90        if method != Method::GET && method != Method::DELETE {
91            rb = rb.json(body);
92        }
93
94        // Build the request.
95        Ok(rb.build()?)
96    }
97
98    /// Create a service.
99    #[tracing::instrument(skip(self))]
100    pub async fn create_service(&self, service: &Service) -> Result<Service> {
101        // Build the request.
102        let request = self.request(
103            Method::POST,
104            "/services",
105            &ServiceObject {
106                service: service.clone(),
107            },
108            None,
109        )?;
110
111        let resp = self.client.execute(request).await?;
112        match resp.status() {
113            StatusCode::OK => (),
114            StatusCode::CREATED => (),
115            StatusCode::ACCEPTED => (),
116            s => {
117                // Try to deserialize the response.
118                let body = resp.text().await?;
119                let err: Error = match serde_json::from_str(&body) {
120                    Ok(j) => j,
121                    Err(_) => {
122                        // If deserialization failed, return the raw response.
123                        Error::Http {
124                            status: s.to_string(),
125                            code: s.as_u16(),
126                            message: body,
127                        }
128                    }
129                };
130
131                return Err(err.into());
132            }
133        };
134
135        let text = resp.text().await?;
136
137        // Try to deserialize the response.
138        let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
139            // If deserialization failed, return the raw response.
140            Error::Json {
141                body: text,
142                message: err.to_string(),
143            }
144        })?;
145
146        Ok(data.service)
147    }
148
149    /// List all services.
150    #[tracing::instrument(skip(self))]
151    pub async fn list_services(&self) -> Result<Vec<Service>> {
152        let mut services: Vec<Service> = Default::default();
153        let mut resp = self.list_services_internal(0).await?;
154
155        services.append(&mut resp.services);
156
157        while resp.more {
158            let offset = resp.offset + LIMIT;
159            resp = self.list_services_internal(offset).await?;
160
161            services.append(&mut resp.services);
162        }
163
164        Ok(services)
165    }
166
167    #[tracing::instrument(skip(self))]
168    async fn list_services_internal(&self, offset: i64) -> Result<ServiceListResponse> {
169        let limit_str = format!("{}", LIMIT);
170        let mut query: Vec<(&str, &str)> = vec![("limit", &limit_str)];
171
172        let offset_str = format!("{}", offset);
173        if offset > 0 {
174            query.push(("offset", &offset_str));
175        }
176
177        // Build the request.
178        let request = self.request(Method::GET, "/services", &(), Some(query))?;
179
180        let resp = self.client.execute(request).await?;
181        match resp.status() {
182            StatusCode::OK => (),
183            s => {
184                // Try to deserialize the response.
185                let body = resp.text().await?;
186                let err: Error = match serde_json::from_str(&body) {
187                    Ok(j) => j,
188                    Err(_) => {
189                        // If deserialization failed, return the raw response.
190                        Error::Http {
191                            status: s.to_string(),
192                            code: s.as_u16(),
193                            message: body,
194                        }
195                    }
196                };
197
198                return Err(err.into());
199            }
200        };
201
202        // Try to deserialize the response.
203        let text = resp.text().await?;
204
205        // Try to deserialize the response.
206        let data: ServiceListResponse = serde_json::from_str(&text).map_err(|err| {
207            // If deserialization failed, return the raw response.
208            Error::Json {
209                body: text,
210                message: err.to_string(),
211            }
212        })?;
213
214        Ok(data)
215    }
216
217    /// List all escalation policies.
218    #[tracing::instrument(skip(self))]
219    pub async fn list_escalation_policies(&self) -> Result<Vec<EscalationPolicy>> {
220        let mut escalation_policies: Vec<EscalationPolicy> = Default::default();
221        let mut resp = self.list_escalation_policies_internal(0).await?;
222
223        escalation_policies.append(&mut resp.escalation_policies);
224
225        while resp.more {
226            let offset = resp.offset + LIMIT;
227            resp = self.list_escalation_policies_internal(offset).await?;
228
229            escalation_policies.append(&mut resp.escalation_policies);
230        }
231
232        Ok(escalation_policies)
233    }
234
235    #[tracing::instrument(skip(self))]
236    async fn list_escalation_policies_internal(&self, offset: i64) -> Result<EscalationPolicyListResponse> {
237        let limit_str = format!("{}", LIMIT);
238        let mut query: Vec<(&str, &str)> = vec![("limit", &limit_str)];
239
240        let offset_str = format!("{}", offset);
241        if offset > 0 {
242            query.push(("offset", &offset_str));
243        }
244
245        // Build the request.
246        let request = self.request(Method::GET, "/escalation_policies", &(), Some(query))?;
247
248        let resp = self.client.execute(request).await?;
249        match resp.status() {
250            StatusCode::OK => (),
251            s => {
252                // Try to deserialize the response.
253                let body = resp.text().await?;
254                let err: Error = match serde_json::from_str(&body) {
255                    Ok(j) => j,
256                    Err(_) => {
257                        // If deserialization failed, return the raw response.
258                        Error::Http {
259                            status: s.to_string(),
260                            code: s.as_u16(),
261                            message: body,
262                        }
263                    }
264                };
265
266                return Err(err.into());
267            }
268        };
269
270        // Try to deserialize the response.
271        let text = resp.text().await?;
272
273        // Try to deserialize the response.
274        let data: EscalationPolicyListResponse = serde_json::from_str(&text).map_err(|err| {
275            // If deserialization failed, return the raw response.
276            Error::Json {
277                body: text,
278                message: err.to_string(),
279            }
280        })?;
281
282        Ok(data)
283    }
284
285    /// Get a service.
286    #[tracing::instrument(skip(self))]
287    pub async fn get_service(&self, id: &str) -> Result<Service> {
288        // Build the request.
289        let request = self.request(Method::GET, &format!("/services/{}", id), &(), None)?;
290
291        let resp = self.client.execute(request).await?;
292        match resp.status() {
293            StatusCode::OK => (),
294            s => {
295                // Try to deserialize the response.
296                let body = resp.text().await?;
297                let err: Error = match serde_json::from_str(&body) {
298                    Ok(j) => j,
299                    Err(_) => {
300                        // If deserialization failed, return the raw response.
301                        Error::Http {
302                            status: s.to_string(),
303                            code: s.as_u16(),
304                            message: body,
305                        }
306                    }
307                };
308
309                return Err(err.into());
310            }
311        };
312
313        // Try to deserialize the response.
314        let text = resp.text().await?;
315
316        // Try to deserialize the response.
317        let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
318            // If deserialization failed, return the raw response.
319            Error::Json {
320                body: text,
321                message: err.to_string(),
322            }
323        })?;
324
325        Ok(data.service)
326    }
327
328    /// Update a service.
329    #[tracing::instrument(skip(self))]
330    pub async fn update_service(&self, service: &Service) -> Result<Service> {
331        // Build the request.
332        let request = self.request(
333            Method::PUT,
334            &format!("/services/{}", service.id),
335            &ServiceObject {
336                service: service.clone(),
337            },
338            None,
339        )?;
340
341        let resp = self.client.execute(request).await?;
342        match resp.status() {
343            StatusCode::OK => (),
344            StatusCode::CREATED => (),
345            StatusCode::ACCEPTED => (),
346            s => {
347                // Try to deserialize the response.
348                let body = resp.text().await?;
349                let err: Error = match serde_json::from_str(&body) {
350                    Ok(j) => j,
351                    Err(_) => {
352                        // If deserialization failed, return the raw response.
353                        Error::Http {
354                            status: s.to_string(),
355                            code: s.as_u16(),
356                            message: body,
357                        }
358                    }
359                };
360
361                return Err(err.into());
362            }
363        };
364
365        // Try to deserialize the response.
366        let text = resp.text().await?;
367
368        // Try to deserialize the response.
369        let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
370            // If deserialization failed, return the raw response.
371            Error::Json {
372                body: text,
373                message: err.to_string(),
374            }
375        })?;
376
377        Ok(data.service)
378    }
379
380    /// Delete a service.
381    #[tracing::instrument(skip(self))]
382    pub async fn delete_service(&self, id: &str) -> Result<()> {
383        // Build the request.
384        let request = self.request(Method::DELETE, &format!("/services/{}", id), &(), None)?;
385
386        let resp = self.client.execute(request).await?;
387        match resp.status() {
388            StatusCode::OK => (),
389            StatusCode::CREATED => (),
390            StatusCode::ACCEPTED => (),
391            StatusCode::NO_CONTENT => (),
392            s => {
393                // Try to deserialize the response.
394                let body = resp.text().await?;
395                let err: Error = match serde_json::from_str(&body) {
396                    Ok(j) => j,
397                    Err(_) => {
398                        // If deserialization failed, return the raw response.
399                        Error::Http {
400                            status: s.to_string(),
401                            code: s.as_u16(),
402                            message: body,
403                        }
404                    }
405                };
406
407                return Err(err.into());
408            }
409        };
410
411        Ok(())
412    }
413}
414
415/// An error for an HTTP request.
416/// This comes from: <https://developers.getbase.com/docs/rest/articles/oauth2/errors>.
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
418#[serde(untagged)]
419pub enum Error {
420    /// An error from the HTTP client.
421    #[error("{status} {code}: {message}")]
422    Http {
423        /// A status string.
424        #[serde(rename = "error_code")]
425        status: String,
426
427        /// The HTTP status code.
428        #[serde(default)]
429        code: u16,
430
431        /// A message string.
432        message: String,
433    },
434    /// An error from Pagerduty.
435    #[error("{error}: {description} {error_uri}")]
436    Pagerduty {
437        /// An error string.
438        error: String,
439
440        /// A description string.
441        description: String,
442
443        /// A URL with more details.
444        error_uri: String,
445    },
446    /// An error from deserialization.
447    #[error("{message}: {body}")]
448    Json {
449        /// The response body.
450        body: String,
451
452        /// The error message.
453        message: String,
454    },
455}