redmine_api/
api.rs

1//! Redmine API
2//!
3//! [`Redmine Documentation`](https://www.redmine.org/projects/redmine/wiki/rest_api)
4//!
5//! - [x] authentication
6//! - [x] pagination
7//!   - [x] add Pageable instances to all types that need them
8//!   - [x] figure out a way to write a general "fetch all pages" function (problem is the different key name in the wrapper)
9//! - [x] impersonation
10//! - [x] attachments
11//! - [x] add all the wrappers I somehow missed
12//!   - [x] check if admin and send_information truly are not part of the user hash in Create/UpdateUser or if the wiki docs are wrong (admin is, send_information is not)
13//! - [x] test include parameters and add relevant data to the return types
14//! - [x] async support
15//!
16//! Potential breaking changes ahead
17//! - [ ] use Enum for sort column
18//! - [ ] typed ids
19//! - [ ] change project_id_or_name to Enum
20//! - [ ] extra filter expressions I overlooked/did not know about
21//! - [ ] parameters that are more flexible than they appear
22
23pub mod attachments;
24pub mod custom_fields;
25pub mod enumerations;
26pub mod files;
27pub mod groups;
28pub mod issue_categories;
29pub mod issue_relations;
30pub mod issue_statuses;
31pub mod issues;
32pub mod my_account;
33pub mod news;
34pub mod project_memberships;
35pub mod projects;
36pub mod queries;
37pub mod roles;
38pub mod search;
39#[cfg(test)]
40pub mod test_helpers;
41pub mod time_entries;
42pub mod trackers;
43pub mod uploads;
44pub mod users;
45pub mod versions;
46pub mod wiki_pages;
47
48use std::str::from_utf8;
49
50use serde::de::DeserializeOwned;
51use serde::Deserialize;
52use serde::Deserializer;
53use serde::Serialize;
54
55use reqwest::Method;
56use std::borrow::Cow;
57
58use reqwest::Url;
59use tracing::{debug, error, trace};
60
61/// main API client object (sync)
62#[derive(derive_more::Debug)]
63pub struct Redmine {
64    /// the reqwest client we use to perform our API requests
65    client: reqwest::blocking::Client,
66    /// the redmine base url
67    redmine_url: Url,
68    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
69    #[debug(skip)]
70    api_key: String,
71    /// the user id we want to impersonate, only works if the API key we use has admin privileges
72    impersonate_user_id: Option<u64>,
73}
74
75/// main API client object (async)
76#[derive(derive_more::Debug)]
77pub struct RedmineAsync {
78    /// the reqwest client we use to perform our API requests
79    client: reqwest::Client,
80    /// the redmine base url
81    redmine_url: Url,
82    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
83    #[debug(skip)]
84    api_key: String,
85    /// the user id we want to impersonate, only works if the API key we use has admin privileges
86    impersonate_user_id: Option<u64>,
87}
88
89/// helper function to parse the redmine URL in the environment variable
90fn parse_url<'de, D>(deserializer: D) -> Result<url::Url, D::Error>
91where
92    D: Deserializer<'de>,
93{
94    let buf = String::deserialize(deserializer)?;
95
96    url::Url::parse(&buf).map_err(serde::de::Error::custom)
97}
98
99/// used to deserialize the required options from the environment
100#[derive(Debug, Clone, serde::Deserialize)]
101struct EnvOptions {
102    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
103    redmine_api_key: String,
104
105    /// the redmine base url
106    #[serde(deserialize_with = "parse_url")]
107    redmine_url: url::Url,
108}
109
110/// Return value from paged requests, includes the actual value as well as
111/// pagination data
112#[derive(Debug, Clone)]
113pub struct ResponsePage<T> {
114    /// The actual value returned by Redmine deserialized into a user provided type
115    pub values: Vec<T>,
116    /// The total number of values that could be returned by requesting all pages
117    pub total_count: u64,
118    /// The offset from the start (zero-based)
119    pub offset: u64,
120    /// How many entries were returned
121    pub limit: u64,
122}
123
124impl Redmine {
125    /// create a [Redmine] object
126    ///
127    /// # Errors
128    ///
129    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
130    pub fn new(redmine_url: url::Url, api_key: &str) -> Result<Self, crate::Error> {
131        #[cfg(not(feature = "rustls-tls"))]
132        let client = reqwest::blocking::Client::new();
133        #[cfg(feature = "rustls-tls")]
134        let client = reqwest::blocking::Client::builder()
135            .use_rustls_tls()
136            .build()?;
137
138        Ok(Self {
139            client,
140            redmine_url,
141            api_key: api_key.to_string(),
142            impersonate_user_id: None,
143        })
144    }
145
146    /// create a [Redmine] object from the environment variables
147    ///
148    /// REDMINE_API_KEY
149    /// REDMINE_URL
150    ///
151    /// # Errors
152    ///
153    /// This will return an error if the environment variables are
154    /// missing or the URL can not be parsed
155    pub fn from_env() -> Result<Self, crate::Error> {
156        let env_options = envy::from_env::<EnvOptions>()?;
157
158        let redmine_url = env_options.redmine_url;
159        let api_key = env_options.redmine_api_key;
160
161        Self::new(redmine_url, &api_key)
162    }
163
164    /// Sets the user id of a user to impersonate in all future API calls
165    ///
166    /// this requires Redmine admin privileges
167    pub fn impersonate_user(&mut self, id: u64) {
168        self.impersonate_user_id = Some(id);
169    }
170
171    /// returns the issue URL for a given issue id
172    ///
173    /// this is mostly for convenience since we are already storing the
174    /// redmine URL and it works entirely on the client
175    #[must_use]
176    #[allow(clippy::missing_panics_doc)]
177    pub fn issue_url(&self, issue_id: u64) -> Url {
178        let Redmine { redmine_url, .. } = self;
179        // we can unwrap here because we know /issues/<number>
180        // parses successfully as an url fragment
181        redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
182    }
183
184    /// internal method for shared logic between the methods below which
185    /// diff in how they parse the response body and how often they call this
186    fn rest(
187        &self,
188        method: reqwest::Method,
189        endpoint: &str,
190        parameters: QueryParams,
191        mime_type_and_body: Option<(&str, Vec<u8>)>,
192    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
193        let Redmine {
194            client,
195            redmine_url,
196            api_key,
197            impersonate_user_id,
198        } = self;
199        let mut url = redmine_url.join(endpoint)?;
200        parameters.add_to_url(&mut url);
201        debug!(%url, %method, "Calling redmine");
202        let req = client
203            .request(method.clone(), url.clone())
204            .header("x-redmine-api-key", api_key);
205        let req = if let Some(user_id) = impersonate_user_id {
206            req.header("X-Redmine-Switch-User", format!("{}", user_id))
207        } else {
208            req
209        };
210        let req = if let Some((mime, data)) = mime_type_and_body {
211            if let Ok(request_body) = from_utf8(&data) {
212                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
213            } else {
214                trace!(
215                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
216                    mime,
217                    data
218                );
219            }
220            req.body(data).header("Content-Type", mime)
221        } else {
222            req
223        };
224        let result = req.send();
225        if let Err(ref e) = result {
226            error!(%url, %method, "Redmine send error: {:?}", e);
227        }
228        let result = result?;
229        let status = result.status();
230        let response_body = result.bytes()?;
231        match from_utf8(&response_body) {
232            Ok(response_body) => {
233                trace!("Response body:\n{}", &response_body);
234            }
235            Err(e) => {
236                trace!(
237                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
238                    &e,
239                    &response_body
240                );
241            }
242        }
243        if status.is_client_error() {
244            error!(%url, %method, "Redmine status error (client error): {:?}", status);
245        } else if status.is_server_error() {
246            error!(%url, %method, "Redmine status error (server error): {:?}", status);
247        }
248        Ok((status, response_body))
249    }
250
251    /// use this with endpoints that have no response body, e.g. those just deleting
252    /// a Redmine object
253    ///
254    /// # Errors
255    ///
256    /// This can return an error if the endpoint returns an error when creating the request
257    /// body or when the web request fails
258    pub fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
259    where
260        E: Endpoint,
261    {
262        let method = endpoint.method();
263        let url = endpoint.endpoint();
264        let parameters = endpoint.parameters();
265        let mime_type_and_body = endpoint.body()?;
266        self.rest(method, &url, parameters, mime_type_and_body)?;
267        Ok(())
268    }
269
270    /// use this with endpoints which return a JSON response but do not support pagination
271    ///
272    /// you can use it with those that support pagination but they will only return the first page
273    ///
274    /// # Errors
275    ///
276    /// This can return an error if the endpoint returns an error when creating the request body,
277    /// when the web request fails or when the response can not be parsed as a JSON object
278    /// into the result type
279    pub fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
280    where
281        E: Endpoint + ReturnsJsonResponse,
282        R: DeserializeOwned + std::fmt::Debug,
283    {
284        let method = endpoint.method();
285        let url = endpoint.endpoint();
286        let parameters = endpoint.parameters();
287        let mime_type_and_body = endpoint.body()?;
288        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
289        if response_body.is_empty() {
290            Err(crate::Error::EmptyResponseBody(status))
291        } else {
292            let result = serde_json::from_slice::<R>(&response_body);
293            if let Ok(ref parsed_response_body) = result {
294                trace!("Parsed response body:\n{:#?}", parsed_response_body);
295            }
296            Ok(result?)
297        }
298    }
299
300    /// use this to get a single page of a paginated JSON response
301    /// # Errors
302    ///
303    /// This can return an error if the endpoint returns an error when creating the
304    /// request body, when the web request fails, when the response can not be parsed
305    /// as a JSON object, when any of the pagination keys or the value key are missing
306    /// in the JSON object or when the values can not be parsed as the result type.
307    pub fn json_response_body_page<E, R>(
308        &self,
309        endpoint: &E,
310        offset: u64,
311        limit: u64,
312    ) -> Result<ResponsePage<R>, crate::Error>
313    where
314        E: Endpoint + ReturnsJsonResponse + Pageable,
315        R: DeserializeOwned + std::fmt::Debug,
316    {
317        let method = endpoint.method();
318        let url = endpoint.endpoint();
319        let mut parameters = endpoint.parameters();
320        parameters.push("offset", offset);
321        parameters.push("limit", limit);
322        let mime_type_and_body = endpoint.body()?;
323        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
324        if response_body.is_empty() {
325            Err(crate::Error::EmptyResponseBody(status))
326        } else {
327            let json_value_response_body: serde_json::Value =
328                serde_json::from_slice(&response_body)?;
329            let json_object_response_body = json_value_response_body.as_object();
330            if let Some(json_object_response_body) = json_object_response_body {
331                let total_count = json_object_response_body
332                    .get("total_count")
333                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
334                    .as_u64()
335                    .ok_or_else(|| {
336                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
337                    })?;
338                let offset = json_object_response_body
339                    .get("offset")
340                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
341                    .as_u64()
342                    .ok_or_else(|| {
343                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
344                    })?;
345                let limit = json_object_response_body
346                    .get("limit")
347                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
348                    .as_u64()
349                    .ok_or_else(|| {
350                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
351                    })?;
352                let response_wrapper_key = endpoint.response_wrapper_key();
353                let inner_response_body = json_object_response_body
354                    .get(&response_wrapper_key)
355                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
356                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
357                if let Ok(ref parsed_response_body) = result {
358                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
359                }
360                Ok(ResponsePage {
361                    values: result?,
362                    total_count,
363                    offset,
364                    limit,
365                })
366            } else {
367                Err(crate::Error::NonObjectResponseBody(status))
368            }
369        }
370    }
371
372    /// use this to get the results for all pages of a paginated JSON response
373    ///
374    /// # Errors
375    ///
376    /// This can return an error if the endpoint returns an error when creating the
377    /// request body, when any of the web requests fails, when the response can not be
378    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
379    /// in the JSON object or when the values can not be parsed as the result type.
380    ///
381    pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
382    where
383        E: Endpoint + ReturnsJsonResponse + Pageable,
384        R: DeserializeOwned + std::fmt::Debug,
385    {
386        let method = endpoint.method();
387        let url = endpoint.endpoint();
388        let mut offset = 0;
389        let limit = 100;
390        let mut total_results = vec![];
391        loop {
392            let mut page_parameters = endpoint.parameters();
393            page_parameters.push("offset", offset);
394            page_parameters.push("limit", limit);
395            let mime_type_and_body = endpoint.body()?;
396            let (status, response_body) =
397                self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
398            if response_body.is_empty() {
399                return Err(crate::Error::EmptyResponseBody(status));
400            }
401            let json_value_response_body: serde_json::Value =
402                serde_json::from_slice(&response_body)?;
403            let json_object_response_body = json_value_response_body.as_object();
404            if let Some(json_object_response_body) = json_object_response_body {
405                let total_count: u64 = json_object_response_body
406                    .get("total_count")
407                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
408                    .as_u64()
409                    .ok_or_else(|| {
410                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
411                    })?;
412                let response_offset: u64 = json_object_response_body
413                    .get("offset")
414                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
415                    .as_u64()
416                    .ok_or_else(|| {
417                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
418                    })?;
419                let response_limit: u64 = json_object_response_body
420                    .get("limit")
421                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
422                    .as_u64()
423                    .ok_or_else(|| {
424                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
425                    })?;
426                let response_wrapper_key = endpoint.response_wrapper_key();
427                let inner_response_body = json_object_response_body
428                    .get(&response_wrapper_key)
429                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
430                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
431                if let Ok(ref parsed_response_body) = result {
432                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
433                }
434                total_results.extend(result?);
435                if total_count < (response_offset + response_limit) {
436                    break;
437                }
438                offset += limit;
439            } else {
440                return Err(crate::Error::NonObjectResponseBody(status));
441            }
442        }
443        Ok(total_results)
444    }
445}
446
447impl RedmineAsync {
448    /// create a [RedmineAsync] object
449    ///
450    /// # Errors
451    ///
452    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
453    pub fn new(redmine_url: url::Url, api_key: &str) -> Result<Self, crate::Error> {
454        #[cfg(not(feature = "rustls-tls"))]
455        let client = reqwest::Client::new();
456        #[cfg(feature = "rustls-tls")]
457        let client = reqwest::Client::builder().use_rustls_tls().build()?;
458
459        Ok(Self {
460            client,
461            redmine_url,
462            api_key: api_key.to_string(),
463            impersonate_user_id: None,
464        })
465    }
466
467    /// create a [RedmineAsync] object from the environment variables
468    ///
469    /// REDMINE_API_KEY
470    /// REDMINE_URL
471    ///
472    /// # Errors
473    ///
474    /// This will return an error if the environment variables are
475    /// missing or the URL can not be parsed
476    pub fn from_env() -> Result<Self, crate::Error> {
477        let env_options = envy::from_env::<EnvOptions>()?;
478
479        let redmine_url = env_options.redmine_url;
480        let api_key = env_options.redmine_api_key;
481
482        Self::new(redmine_url, &api_key)
483    }
484
485    /// Sets the user id of a user to impersonate in all future API calls
486    ///
487    /// this requires Redmine admin privileges
488    pub fn impersonate_user(&mut self, id: u64) {
489        self.impersonate_user_id = Some(id);
490    }
491
492    /// returns the issue URL for a given issue id
493    ///
494    /// this is mostly for convenience since we are already storing the
495    /// redmine URL and it works entirely on the client
496    #[must_use]
497    #[allow(clippy::missing_panics_doc)]
498    pub fn issue_url(&self, issue_id: u64) -> Url {
499        let RedmineAsync { redmine_url, .. } = self;
500        // we can unwrap here because we know /issues/<number>
501        // parses successfully as an url fragment
502        redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
503    }
504
505    /// internal method for shared logic between the methods below which
506    /// diff in how they parse the response body and how often they call this
507    async fn rest(
508        &self,
509        method: reqwest::Method,
510        endpoint: &str,
511        parameters: QueryParams<'_>,
512        mime_type_and_body: Option<(&str, Vec<u8>)>,
513    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
514        let RedmineAsync {
515            client,
516            redmine_url,
517            api_key,
518            impersonate_user_id,
519        } = self;
520        let mut url = redmine_url.join(endpoint)?;
521        parameters.add_to_url(&mut url);
522        debug!(%url, %method, "Calling redmine");
523        let req = client
524            .request(method.clone(), url.clone())
525            .header("x-redmine-api-key", api_key);
526        let req = if let Some(user_id) = impersonate_user_id {
527            req.header("X-Redmine-Switch-User", format!("{}", user_id))
528        } else {
529            req
530        };
531        let req = if let Some((mime, data)) = mime_type_and_body {
532            if let Ok(request_body) = from_utf8(&data) {
533                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
534            } else {
535                trace!(
536                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
537                    mime,
538                    data
539                );
540            }
541            req.body(data).header("Content-Type", mime)
542        } else {
543            req
544        };
545        let result = req.send().await;
546        if let Err(ref e) = result {
547            error!(%url, %method, "Redmine send error: {:?}", e);
548        }
549        let result = result?;
550        let status = result.status();
551        let response_body = result.bytes().await?;
552        match from_utf8(&response_body) {
553            Ok(response_body) => {
554                trace!("Response body:\n{}", &response_body);
555            }
556            Err(e) => {
557                trace!(
558                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
559                    &e,
560                    &response_body
561                );
562            }
563        }
564        if status.is_client_error() {
565            error!(%url, %method, "Redmine status error (client error): {:?}", status);
566        } else if status.is_server_error() {
567            error!(%url, %method, "Redmine status error (server error): {:?}", status);
568        }
569        Ok((status, response_body))
570    }
571
572    /// use this with endpoints that have no response body, e.g. those just deleting
573    /// a Redmine object
574    ///
575    /// # Errors
576    ///
577    /// This can return an error if the endpoint returns an error when creating the request
578    /// body or when the web request fails
579    pub async fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
580    where
581        E: Endpoint,
582    {
583        let method = endpoint.method();
584        let url = endpoint.endpoint();
585        let parameters = endpoint.parameters();
586        let mime_type_and_body = endpoint.body()?;
587        self.rest(method, &url, parameters, mime_type_and_body)
588            .await?;
589        Ok(())
590    }
591
592    /// use this with endpoints which return a JSON response but do not support pagination
593    ///
594    /// you can use it with those that support pagination but they will only return the first page
595    ///
596    /// # Errors
597    ///
598    /// This can return an error if the endpoint returns an error when creating the request body,
599    /// when the web request fails or when the response can not be parsed as a JSON object
600    /// into the result type
601    pub async fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
602    where
603        E: Endpoint + ReturnsJsonResponse,
604        R: DeserializeOwned + std::fmt::Debug,
605    {
606        let method = endpoint.method();
607        let url = endpoint.endpoint();
608        let parameters = endpoint.parameters();
609        let mime_type_and_body = endpoint.body()?;
610        let (status, response_body) = self
611            .rest(method, &url, parameters, mime_type_and_body)
612            .await?;
613        if response_body.is_empty() {
614            Err(crate::Error::EmptyResponseBody(status))
615        } else {
616            let result = serde_json::from_slice::<R>(&response_body);
617            if let Ok(ref parsed_response_body) = result {
618                trace!("Parsed response body:\n{:#?}", parsed_response_body);
619            }
620            Ok(result?)
621        }
622    }
623
624    /// use this to get a single page of a paginated JSON response
625    /// # Errors
626    ///
627    /// This can return an error if the endpoint returns an error when creating the
628    /// request body, when the web request fails, when the response can not be parsed
629    /// as a JSON object, when any of the pagination keys or the value key are missing
630    /// in the JSON object or when the values can not be parsed as the result type.
631    pub async fn json_response_body_page<E, R>(
632        &self,
633        endpoint: &E,
634        offset: u64,
635        limit: u64,
636    ) -> Result<ResponsePage<R>, crate::Error>
637    where
638        E: Endpoint + ReturnsJsonResponse + Pageable,
639        R: DeserializeOwned + std::fmt::Debug,
640    {
641        let method = endpoint.method();
642        let url = endpoint.endpoint();
643        let mut parameters = endpoint.parameters();
644        parameters.push("offset", offset);
645        parameters.push("limit", limit);
646        let mime_type_and_body = endpoint.body()?;
647        let (status, response_body) = self
648            .rest(method, &url, parameters, mime_type_and_body)
649            .await?;
650        if response_body.is_empty() {
651            Err(crate::Error::EmptyResponseBody(status))
652        } else {
653            let json_value_response_body: serde_json::Value =
654                serde_json::from_slice(&response_body)?;
655            let json_object_response_body = json_value_response_body.as_object();
656            if let Some(json_object_response_body) = json_object_response_body {
657                let total_count = json_object_response_body
658                    .get("total_count")
659                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
660                    .as_u64()
661                    .ok_or_else(|| {
662                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
663                    })?;
664                let offset = json_object_response_body
665                    .get("offset")
666                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
667                    .as_u64()
668                    .ok_or_else(|| {
669                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
670                    })?;
671                let limit = json_object_response_body
672                    .get("limit")
673                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
674                    .as_u64()
675                    .ok_or_else(|| {
676                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
677                    })?;
678                let response_wrapper_key = endpoint.response_wrapper_key();
679                let inner_response_body = json_object_response_body
680                    .get(&response_wrapper_key)
681                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
682                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
683                if let Ok(ref parsed_response_body) = result {
684                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
685                }
686                Ok(ResponsePage {
687                    values: result?,
688                    total_count,
689                    offset,
690                    limit,
691                })
692            } else {
693                Err(crate::Error::NonObjectResponseBody(status))
694            }
695        }
696    }
697
698    /// use this to get the results for all pages of a paginated JSON response
699    ///
700    /// # Errors
701    ///
702    /// This can return an error if the endpoint returns an error when creating the
703    /// request body, when any of the web requests fails, when the response can not be
704    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
705    /// in the JSON object or when the values can not be parsed as the result type.
706    ///
707    pub async fn json_response_body_all_pages<E, R>(
708        &self,
709        endpoint: &E,
710    ) -> Result<Vec<R>, crate::Error>
711    where
712        E: Endpoint + ReturnsJsonResponse + Pageable,
713        R: DeserializeOwned + std::fmt::Debug,
714    {
715        let method = endpoint.method();
716        let url = endpoint.endpoint();
717        let mut offset = 0;
718        let limit = 100;
719        let mut total_results = vec![];
720        loop {
721            let mut page_parameters = endpoint.parameters();
722            page_parameters.push("offset", offset);
723            page_parameters.push("limit", limit);
724            let mime_type_and_body = endpoint.body()?;
725            let (status, response_body) = self
726                .rest(method.clone(), &url, page_parameters, mime_type_and_body)
727                .await?;
728            if response_body.is_empty() {
729                return Err(crate::Error::EmptyResponseBody(status));
730            }
731            let json_value_response_body: serde_json::Value =
732                serde_json::from_slice(&response_body)?;
733            let json_object_response_body = json_value_response_body.as_object();
734            if let Some(json_object_response_body) = json_object_response_body {
735                let total_count: u64 = json_object_response_body
736                    .get("total_count")
737                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
738                    .as_u64()
739                    .ok_or_else(|| {
740                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
741                    })?;
742                let response_offset: u64 = json_object_response_body
743                    .get("offset")
744                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
745                    .as_u64()
746                    .ok_or_else(|| {
747                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
748                    })?;
749                let response_limit: u64 = json_object_response_body
750                    .get("limit")
751                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
752                    .as_u64()
753                    .ok_or_else(|| {
754                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
755                    })?;
756                let response_wrapper_key = endpoint.response_wrapper_key();
757                let inner_response_body = json_object_response_body
758                    .get(&response_wrapper_key)
759                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
760                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
761                if let Ok(ref parsed_response_body) = result {
762                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
763                }
764                total_results.extend(result?);
765                if total_count < (response_offset + response_limit) {
766                    break;
767                }
768                offset += limit;
769            } else {
770                return Err(crate::Error::NonObjectResponseBody(status));
771            }
772        }
773        Ok(total_results)
774    }
775}
776
777/// A trait representing a parameter value.
778pub trait ParamValue<'a> {
779    #[allow(clippy::wrong_self_convention)]
780    /// The parameter value as a string.
781    fn as_value(&self) -> Cow<'a, str>;
782}
783
784impl ParamValue<'static> for bool {
785    fn as_value(&self) -> Cow<'static, str> {
786        if *self {
787            "true".into()
788        } else {
789            "false".into()
790        }
791    }
792}
793
794impl<'a> ParamValue<'a> for &'a str {
795    fn as_value(&self) -> Cow<'a, str> {
796        (*self).into()
797    }
798}
799
800impl ParamValue<'static> for String {
801    fn as_value(&self) -> Cow<'static, str> {
802        self.clone().into()
803    }
804}
805
806impl<'a> ParamValue<'a> for &'a String {
807    fn as_value(&self) -> Cow<'a, str> {
808        (*self).into()
809    }
810}
811
812/// serialize a [`Vec<T>`] where T implements [ToString] as a string
813/// of comma-separated values
814impl<T> ParamValue<'static> for Vec<T>
815where
816    T: ToString,
817{
818    fn as_value(&self) -> Cow<'static, str> {
819        self.iter()
820            .map(|e| e.to_string())
821            .collect::<Vec<_>>()
822            .join(",")
823            .into()
824    }
825}
826
827/// serialize a [`&Vec<T>`](Vec<T>) where T implements [ToString] as a string
828/// of comma-separated values
829impl<'a, T> ParamValue<'a> for &'a Vec<T>
830where
831    T: ToString,
832{
833    fn as_value(&self) -> Cow<'a, str> {
834        self.iter()
835            .map(|e| e.to_string())
836            .collect::<Vec<_>>()
837            .join(",")
838            .into()
839    }
840}
841
842impl<'a> ParamValue<'a> for Cow<'a, str> {
843    fn as_value(&self) -> Cow<'a, str> {
844        self.clone()
845    }
846}
847
848impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
849    fn as_value(&self) -> Cow<'a, str> {
850        (*self).clone()
851    }
852}
853
854impl ParamValue<'static> for u64 {
855    fn as_value(&self) -> Cow<'static, str> {
856        format!("{}", self).into()
857    }
858}
859
860impl ParamValue<'static> for f64 {
861    fn as_value(&self) -> Cow<'static, str> {
862        format!("{}", self).into()
863    }
864}
865
866impl ParamValue<'static> for time::OffsetDateTime {
867    fn as_value(&self) -> Cow<'static, str> {
868        self.format(&time::format_description::well_known::Rfc3339)
869            .unwrap()
870            .into()
871    }
872}
873
874impl ParamValue<'static> for time::Date {
875    fn as_value(&self) -> Cow<'static, str> {
876        let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
877        self.format(&format).unwrap().into()
878    }
879}
880
881/// A structure for query parameters.
882#[derive(Debug, Default, Clone)]
883pub struct QueryParams<'a> {
884    /// the actual parameters
885    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
886}
887
888impl<'a> QueryParams<'a> {
889    /// Push a single parameter.
890    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
891    where
892        K: Into<Cow<'a, str>>,
893        V: ParamValue<'b>,
894        'b: 'a,
895    {
896        self.params.push((key.into(), value.as_value()));
897        self
898    }
899
900    /// Push a single parameter.
901    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
902    where
903        K: Into<Cow<'a, str>>,
904        V: ParamValue<'b>,
905        'b: 'a,
906    {
907        if let Some(value) = value {
908            self.params.push((key.into(), value.as_value()));
909        }
910        self
911    }
912
913    /// Push a set of parameters.
914    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
915    where
916        I: Iterator<Item = (K, V)>,
917        K: Into<Cow<'a, str>>,
918        V: ParamValue<'b>,
919        'b: 'a,
920    {
921        self.params
922            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
923        self
924    }
925
926    /// Add the parameters to a URL.
927    pub fn add_to_url(&self, url: &mut Url) {
928        let mut pairs = url.query_pairs_mut();
929        pairs.extend_pairs(self.params.iter());
930    }
931}
932
933/// A trait for providing the necessary information for a single REST API endpoint.
934pub trait Endpoint {
935    /// The HTTP method to use for the endpoint.
936    fn method(&self) -> Method;
937    /// The path to the endpoint.
938    fn endpoint(&self) -> Cow<'static, str>;
939
940    /// Query parameters for the endpoint.
941    fn parameters(&self) -> QueryParams {
942        QueryParams::default()
943    }
944
945    /// The body for the endpoint.
946    ///
947    /// Returns the `Content-Encoding` header for the data as well as the data itself.
948    ///
949    /// # Errors
950    ///
951    /// The default implementation will never return an error
952    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
953        Ok(None)
954    }
955}
956
957/// A trait to indicate that an endpoint is expected to return a JSON result
958pub trait ReturnsJsonResponse {}
959
960/// A trait to indicate that an endpoint is pageable.
961pub trait Pageable {
962    /// returns the name of the key in the response that contains the list of results
963    fn response_wrapper_key(&self) -> String;
964}
965
966/// helper to parse created_on and updated_on in the correct format
967/// (default time serde implementation seems to use a different format)
968///
969/// # Errors
970///
971/// This will return an error if the underlying string can not be deserialized or
972/// can not be parsed as an RFC3339 date and time
973pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
974where
975    D: serde::Deserializer<'de>,
976{
977    let s = String::deserialize(deserializer)?;
978
979    time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
980        .map_err(serde::de::Error::custom)
981}
982
983/// helper to serialize created_on and updated_on in the correct format
984/// (default time serde implementation seems to use a different format)
985///
986/// # Errors
987///
988/// This will return an error if the date time can not be formatted as an RFC3339
989/// date time or the resulting string can not be serialized
990pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
991where
992    S: serde::Serializer,
993{
994    let s = t
995        .format(&time::format_description::well_known::Rfc3339)
996        .map_err(serde::ser::Error::custom)?;
997
998    s.serialize(serializer)
999}
1000
1001/// helper to parse created_on and updated_on in the correct format
1002/// (default time serde implementation seems to use a different format)
1003///
1004/// # Errors
1005///
1006/// This will return an error if the underlying string can not be deserialized
1007/// or it can not be parsed as an RFC3339 date and time
1008pub fn deserialize_optional_rfc3339<'de, D>(
1009    deserializer: D,
1010) -> Result<Option<time::OffsetDateTime>, D::Error>
1011where
1012    D: serde::Deserializer<'de>,
1013{
1014    let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1015
1016    if let Some(s) = s {
1017        Ok(Some(
1018            time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1019                .map_err(serde::de::Error::custom)?,
1020        ))
1021    } else {
1022        Ok(None)
1023    }
1024}
1025
1026/// helper to serialize created_on and updated_on in the correct format
1027/// (default time serde implementation seems to use a different format)
1028///
1029/// # Errors
1030///
1031/// This will return an error if the parameter can not be formatted as RFC3339
1032/// or the resulting string can not be serialized
1033pub fn serialize_optional_rfc3339<S>(
1034    t: &Option<time::OffsetDateTime>,
1035    serializer: S,
1036) -> Result<S::Ok, S::Error>
1037where
1038    S: serde::Serializer,
1039{
1040    if let Some(t) = t {
1041        let s = t
1042            .format(&time::format_description::well_known::Rfc3339)
1043            .map_err(serde::ser::Error::custom)?;
1044
1045        s.serialize(serializer)
1046    } else {
1047        let n: Option<String> = None;
1048        n.serialize(serializer)
1049    }
1050}