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