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