1#![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#[derive(Debug, Clone)]
17pub struct Client {
18 client: reqwest_middleware::ClientWithMiddleware,
19 token: String,
20}
21
22pub const DEFAULT_HOST: &str = "https://api.pagerduty.com";
24pub const LIMIT: i64 = 30;
26
27impl Client {
28 #[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 .with(reqwest_tracing::TracingMiddleware::default())
41 .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 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 rb = rb.header("Authorization", &format!("Token token={}", self.token));
78
79 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 if method != Method::GET && method != Method::DELETE {
91 rb = rb.json(body);
92 }
93
94 Ok(rb.build()?)
96 }
97
98 #[tracing::instrument(skip(self))]
100 pub async fn create_service(&self, service: &Service) -> Result<Service> {
101 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 let body = resp.text().await?;
119 let err: Error = match serde_json::from_str(&body) {
120 Ok(j) => j,
121 Err(_) => {
122 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 let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
139 Error::Json {
141 body: text,
142 message: err.to_string(),
143 }
144 })?;
145
146 Ok(data.service)
147 }
148
149 #[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 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 let body = resp.text().await?;
186 let err: Error = match serde_json::from_str(&body) {
187 Ok(j) => j,
188 Err(_) => {
189 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 let text = resp.text().await?;
204
205 let data: ServiceListResponse = serde_json::from_str(&text).map_err(|err| {
207 Error::Json {
209 body: text,
210 message: err.to_string(),
211 }
212 })?;
213
214 Ok(data)
215 }
216
217 #[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 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 let body = resp.text().await?;
254 let err: Error = match serde_json::from_str(&body) {
255 Ok(j) => j,
256 Err(_) => {
257 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 let text = resp.text().await?;
272
273 let data: EscalationPolicyListResponse = serde_json::from_str(&text).map_err(|err| {
275 Error::Json {
277 body: text,
278 message: err.to_string(),
279 }
280 })?;
281
282 Ok(data)
283 }
284
285 #[tracing::instrument(skip(self))]
287 pub async fn get_service(&self, id: &str) -> Result<Service> {
288 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 let body = resp.text().await?;
297 let err: Error = match serde_json::from_str(&body) {
298 Ok(j) => j,
299 Err(_) => {
300 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 let text = resp.text().await?;
315
316 let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
318 Error::Json {
320 body: text,
321 message: err.to_string(),
322 }
323 })?;
324
325 Ok(data.service)
326 }
327
328 #[tracing::instrument(skip(self))]
330 pub async fn update_service(&self, service: &Service) -> Result<Service> {
331 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 let body = resp.text().await?;
349 let err: Error = match serde_json::from_str(&body) {
350 Ok(j) => j,
351 Err(_) => {
352 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 let text = resp.text().await?;
367
368 let data: ServiceObject = serde_json::from_str(&text).map_err(|err| {
370 Error::Json {
372 body: text,
373 message: err.to_string(),
374 }
375 })?;
376
377 Ok(data.service)
378 }
379
380 #[tracing::instrument(skip(self))]
382 pub async fn delete_service(&self, id: &str) -> Result<()> {
383 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 let body = resp.text().await?;
395 let err: Error = match serde_json::from_str(&body) {
396 Ok(j) => j,
397 Err(_) => {
398 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
418#[serde(untagged)]
419pub enum Error {
420 #[error("{status} {code}: {message}")]
422 Http {
423 #[serde(rename = "error_code")]
425 status: String,
426
427 #[serde(default)]
429 code: u16,
430
431 message: String,
433 },
434 #[error("{error}: {description} {error_uri}")]
436 Pagerduty {
437 error: String,
439
440 description: String,
442
443 error_uri: String,
445 },
446 #[error("{message}: {body}")]
448 Json {
449 body: String,
451
452 message: String,
454 },
455}