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 futures::future::FutureExt as _;
49
50use std::str::from_utf8;
51
52use serde::Deserialize;
53use serde::Deserializer;
54use serde::Serialize;
55use serde::de::DeserializeOwned;
56
57use reqwest::Method;
58use std::borrow::Cow;
59
60use reqwest::Url;
61use tracing::{debug, error, trace};
62
63/// main API client object (sync)
64#[derive(derive_more::Debug)]
65pub struct Redmine {
66    /// the reqwest client we use to perform our API requests
67    client: reqwest::blocking::Client,
68    /// the redmine base url
69    redmine_url: Url,
70    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
71    #[debug(skip)]
72    api_key: String,
73    /// the user id we want to impersonate, only works if the API key we use has admin privileges
74    impersonate_user_id: Option<u64>,
75}
76
77/// main API client object (async)
78#[derive(derive_more::Debug)]
79pub struct RedmineAsync {
80    /// the reqwest client we use to perform our API requests
81    client: reqwest::Client,
82    /// the redmine base url
83    redmine_url: Url,
84    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
85    #[debug(skip)]
86    api_key: String,
87    /// the user id we want to impersonate, only works if the API key we use has admin privileges
88    impersonate_user_id: Option<u64>,
89}
90
91/// helper function to parse the redmine URL in the environment variable
92fn parse_url<'de, D>(deserializer: D) -> Result<url::Url, D::Error>
93where
94    D: Deserializer<'de>,
95{
96    let buf = String::deserialize(deserializer)?;
97
98    url::Url::parse(&buf).map_err(serde::de::Error::custom)
99}
100
101/// used to deserialize the required options from the environment
102#[derive(Debug, Clone, serde::Deserialize)]
103struct EnvOptions {
104    /// a redmine API key, usually 40 hex digits where the letters (a-f) are lower case
105    redmine_api_key: String,
106
107    /// the redmine base url
108    #[serde(deserialize_with = "parse_url")]
109    redmine_url: url::Url,
110}
111
112/// Return value from paged requests, includes the actual value as well as
113/// pagination data
114#[derive(Debug, Clone)]
115pub struct ResponsePage<T> {
116    /// The actual value returned by Redmine deserialized into a user provided type
117    pub values: Vec<T>,
118    /// The total number of values that could be returned by requesting all pages
119    pub total_count: u64,
120    /// The offset from the start (zero-based)
121    pub offset: u64,
122    /// How many entries were returned
123    pub limit: u64,
124}
125
126impl Redmine {
127    /// create a [Redmine] object
128    ///
129    /// # Errors
130    ///
131    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
132    pub fn new(
133        client: reqwest::blocking::Client,
134        redmine_url: url::Url,
135        api_key: &str,
136    ) -> Result<Self, crate::Error> {
137        Ok(Self {
138            client,
139            redmine_url,
140            api_key: api_key.to_string(),
141            impersonate_user_id: None,
142        })
143    }
144
145    /// create a [Redmine] object from the environment variables
146    ///
147    /// REDMINE_API_KEY
148    /// REDMINE_URL
149    ///
150    /// # Errors
151    ///
152    /// This will return an error if the environment variables are
153    /// missing or the URL can not be parsed
154    pub fn from_env(client: reqwest::blocking::Client) -> Result<Self, crate::Error> {
155        let env_options = envy::from_env::<EnvOptions>()?;
156
157        let redmine_url = env_options.redmine_url;
158        let api_key = env_options.redmine_api_key;
159
160        Self::new(client, redmine_url, &api_key)
161    }
162
163    /// Sets the user id of a user to impersonate in all future API calls
164    ///
165    /// this requires Redmine admin privileges
166    pub fn impersonate_user(&mut self, id: u64) {
167        self.impersonate_user_id = Some(id);
168    }
169
170    /// returns the issue URL for a given issue id
171    ///
172    /// this is mostly for convenience since we are already storing the
173    /// redmine URL and it works entirely on the client
174    #[must_use]
175    #[allow(clippy::missing_panics_doc)]
176    pub fn issue_url(&self, issue_id: u64) -> Url {
177        let Redmine { redmine_url, .. } = self;
178        // we can unwrap here because we know /issues/<number>
179        // parses successfully as an url fragment
180        redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
181    }
182
183    /// internal method for shared logic between the methods below which
184    /// diff in how they parse the response body and how often they call this
185    fn rest(
186        &self,
187        method: reqwest::Method,
188        endpoint: &str,
189        parameters: QueryParams,
190        mime_type_and_body: Option<(&str, Vec<u8>)>,
191    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
192        let Redmine {
193            client,
194            redmine_url,
195            api_key,
196            impersonate_user_id,
197        } = self;
198        let mut url = redmine_url.join(endpoint)?;
199        parameters.add_to_url(&mut url);
200        debug!(%url, %method, "Calling redmine");
201        let req = client
202            .request(method.clone(), url.clone())
203            .header("x-redmine-api-key", api_key);
204        let req = if let Some(user_id) = impersonate_user_id {
205            req.header("X-Redmine-Switch-User", format!("{user_id}"))
206        } else {
207            req
208        };
209        let req = if let Some((mime, data)) = mime_type_and_body {
210            if let Ok(request_body) = from_utf8(&data) {
211                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
212            } else {
213                trace!(
214                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
215                    mime, data
216                );
217            }
218            req.body(data).header("Content-Type", mime)
219        } else {
220            req
221        };
222        let result = req.send();
223        if let Err(ref e) = result {
224            error!(%url, %method, "Redmine send error: {:?}", e);
225        }
226        let result = result?;
227        let status = result.status();
228        let response_body = result.bytes()?;
229        match from_utf8(&response_body) {
230            Ok(response_body) => {
231                trace!("Response body:\n{}", &response_body);
232            }
233            Err(e) => {
234                trace!(
235                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
236                    &e, &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    /// use this to get the results for all pages of a paginated JSON response
442    /// as an Iterator
443    pub fn json_response_body_all_pages_iter<'a, 'e, 'i, E, R>(
444        &'a self,
445        endpoint: &'e E,
446    ) -> AllPages<'i, E, R>
447    where
448        E: Endpoint + ReturnsJsonResponse + Pageable,
449        R: DeserializeOwned + std::fmt::Debug,
450        'a: 'i,
451        'e: 'i,
452    {
453        AllPages::new(self, endpoint)
454    }
455}
456
457impl RedmineAsync {
458    /// create a [RedmineAsync] object
459    ///
460    /// # Errors
461    ///
462    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
463    pub fn new(
464        client: reqwest::Client,
465        redmine_url: url::Url,
466        api_key: &str,
467    ) -> Result<std::sync::Arc<Self>, crate::Error> {
468        Ok(std::sync::Arc::new(Self {
469            client,
470            redmine_url,
471            api_key: api_key.to_string(),
472            impersonate_user_id: None,
473        }))
474    }
475
476    /// create a [RedmineAsync] object from the environment variables
477    ///
478    /// REDMINE_API_KEY
479    /// REDMINE_URL
480    ///
481    /// # Errors
482    ///
483    /// This will return an error if the environment variables are
484    /// missing or the URL can not be parsed
485    pub fn from_env(client: reqwest::Client) -> Result<std::sync::Arc<Self>, crate::Error> {
486        let env_options = envy::from_env::<EnvOptions>()?;
487
488        let redmine_url = env_options.redmine_url;
489        let api_key = env_options.redmine_api_key;
490
491        Self::new(client, redmine_url, &api_key)
492    }
493
494    /// Sets the user id of a user to impersonate in all future API calls
495    ///
496    /// this requires Redmine admin privileges
497    pub fn impersonate_user(&mut self, id: u64) {
498        self.impersonate_user_id = Some(id);
499    }
500
501    /// returns the issue URL for a given issue id
502    ///
503    /// this is mostly for convenience since we are already storing the
504    /// redmine URL and it works entirely on the client
505    #[must_use]
506    #[allow(clippy::missing_panics_doc)]
507    pub fn issue_url(&self, issue_id: u64) -> Url {
508        let RedmineAsync { redmine_url, .. } = self;
509        // we can unwrap here because we know /issues/<number>
510        // parses successfully as an url fragment
511        redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
512    }
513
514    /// internal method for shared logic between the methods below which
515    /// diff in how they parse the response body and how often they call this
516    async fn rest(
517        self: std::sync::Arc<Self>,
518        method: reqwest::Method,
519        endpoint: &str,
520        parameters: QueryParams<'_>,
521        mime_type_and_body: Option<(&str, Vec<u8>)>,
522    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
523        let RedmineAsync {
524            client,
525            redmine_url,
526            api_key,
527            impersonate_user_id,
528        } = self.as_ref();
529        let mut url = redmine_url.join(endpoint)?;
530        parameters.add_to_url(&mut url);
531        debug!(%url, %method, "Calling redmine");
532        let req = client
533            .request(method.clone(), url.clone())
534            .header("x-redmine-api-key", api_key);
535        let req = if let Some(user_id) = impersonate_user_id {
536            req.header("X-Redmine-Switch-User", format!("{user_id}"))
537        } else {
538            req
539        };
540        let req = if let Some((mime, data)) = mime_type_and_body {
541            if let Ok(request_body) = from_utf8(&data) {
542                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
543            } else {
544                trace!(
545                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
546                    mime, data
547                );
548            }
549            req.body(data).header("Content-Type", mime)
550        } else {
551            req
552        };
553        let result = req.send().await;
554        if let Err(ref e) = result {
555            error!(%url, %method, "Redmine send error: {:?}", e);
556        }
557        let result = result?;
558        let status = result.status();
559        let response_body = result.bytes().await?;
560        match from_utf8(&response_body) {
561            Ok(response_body) => {
562                trace!("Response body:\n{}", &response_body);
563            }
564            Err(e) => {
565                trace!(
566                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
567                    &e, &response_body
568                );
569            }
570        }
571        if status.is_client_error() {
572            error!(%url, %method, "Redmine status error (client error): {:?}", status);
573        } else if status.is_server_error() {
574            error!(%url, %method, "Redmine status error (server error): {:?}", status);
575        }
576        Ok((status, response_body))
577    }
578
579    /// use this with endpoints that have no response body, e.g. those just deleting
580    /// a Redmine object
581    ///
582    /// # Errors
583    ///
584    /// This can return an error if the endpoint returns an error when creating the request
585    /// body or when the web request fails
586    pub async fn ignore_response_body<E>(
587        self: std::sync::Arc<Self>,
588        endpoint: impl EndpointParameter<E>,
589    ) -> Result<(), crate::Error>
590    where
591        E: Endpoint,
592    {
593        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
594        let method = endpoint.method();
595        let url = endpoint.endpoint();
596        let parameters = endpoint.parameters();
597        let mime_type_and_body = endpoint.body()?;
598        self.rest(method, &url, parameters, mime_type_and_body)
599            .await?;
600        Ok(())
601    }
602
603    /// use this with endpoints which return a JSON response but do not support pagination
604    ///
605    /// you can use it with those that support pagination but they will only return the first page
606    ///
607    /// # Errors
608    ///
609    /// This can return an error if the endpoint returns an error when creating the request body,
610    /// when the web request fails or when the response can not be parsed as a JSON object
611    /// into the result type
612    pub async fn json_response_body<E, R>(
613        self: std::sync::Arc<Self>,
614        endpoint: impl EndpointParameter<E>,
615    ) -> Result<R, crate::Error>
616    where
617        E: Endpoint + ReturnsJsonResponse + NoPagination,
618        R: DeserializeOwned + std::fmt::Debug,
619    {
620        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
621        let method = endpoint.method();
622        let url = endpoint.endpoint();
623        let parameters = endpoint.parameters();
624        let mime_type_and_body = endpoint.body()?;
625        let (status, response_body) = self
626            .rest(method, &url, parameters, mime_type_and_body)
627            .await?;
628        if response_body.is_empty() {
629            Err(crate::Error::EmptyResponseBody(status))
630        } else {
631            let result = serde_json::from_slice::<R>(&response_body);
632            if let Ok(ref parsed_response_body) = result {
633                trace!("Parsed response body:\n{:#?}", parsed_response_body);
634            }
635            Ok(result?)
636        }
637    }
638
639    /// use this to get a single page of a paginated JSON response
640    /// # Errors
641    ///
642    /// This can return an error if the endpoint returns an error when creating the
643    /// request body, when the web request fails, when the response can not be parsed
644    /// as a JSON object, when any of the pagination keys or the value key are missing
645    /// in the JSON object or when the values can not be parsed as the result type.
646    pub async fn json_response_body_page<E, R>(
647        self: std::sync::Arc<Self>,
648        endpoint: impl EndpointParameter<E>,
649        offset: u64,
650        limit: u64,
651    ) -> Result<ResponsePage<R>, crate::Error>
652    where
653        E: Endpoint + ReturnsJsonResponse + Pageable,
654        R: DeserializeOwned + std::fmt::Debug,
655    {
656        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
657        let method = endpoint.method();
658        let url = endpoint.endpoint();
659        let mut parameters = endpoint.parameters();
660        parameters.push("offset", offset);
661        parameters.push("limit", limit);
662        let mime_type_and_body = endpoint.body()?;
663        let (status, response_body) = self
664            .rest(method, &url, parameters, mime_type_and_body)
665            .await?;
666        if response_body.is_empty() {
667            Err(crate::Error::EmptyResponseBody(status))
668        } else {
669            let json_value_response_body: serde_json::Value =
670                serde_json::from_slice(&response_body)?;
671            let json_object_response_body = json_value_response_body.as_object();
672            if let Some(json_object_response_body) = json_object_response_body {
673                let total_count = json_object_response_body
674                    .get("total_count")
675                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
676                    .as_u64()
677                    .ok_or_else(|| {
678                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
679                    })?;
680                let offset = json_object_response_body
681                    .get("offset")
682                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
683                    .as_u64()
684                    .ok_or_else(|| {
685                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
686                    })?;
687                let limit = json_object_response_body
688                    .get("limit")
689                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
690                    .as_u64()
691                    .ok_or_else(|| {
692                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
693                    })?;
694                let response_wrapper_key = endpoint.response_wrapper_key();
695                let inner_response_body = json_object_response_body
696                    .get(&response_wrapper_key)
697                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
698                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
699                if let Ok(ref parsed_response_body) = result {
700                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
701                }
702                Ok(ResponsePage {
703                    values: result?,
704                    total_count,
705                    offset,
706                    limit,
707                })
708            } else {
709                Err(crate::Error::NonObjectResponseBody(status))
710            }
711        }
712    }
713
714    /// use this to get the results for all pages of a paginated JSON response
715    ///
716    /// # Errors
717    ///
718    /// This can return an error if the endpoint returns an error when creating the
719    /// request body, when any of the web requests fails, when the response can not be
720    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
721    /// in the JSON object or when the values can not be parsed as the result type.
722    ///
723    pub async fn json_response_body_all_pages<E, R>(
724        self: std::sync::Arc<Self>,
725        endpoint: impl EndpointParameter<E>,
726    ) -> Result<Vec<R>, crate::Error>
727    where
728        E: Endpoint + ReturnsJsonResponse + Pageable,
729        R: DeserializeOwned + std::fmt::Debug,
730    {
731        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
732        let method = endpoint.method();
733        let url = endpoint.endpoint();
734        let mut offset = 0;
735        let limit = 100;
736        let mut total_results = vec![];
737        loop {
738            let mut page_parameters = endpoint.parameters();
739            page_parameters.push("offset", offset);
740            page_parameters.push("limit", limit);
741            let mime_type_and_body = endpoint.body()?;
742            let (status, response_body) = self
743                .clone()
744                .rest(method.clone(), &url, page_parameters, mime_type_and_body)
745                .await?;
746            if response_body.is_empty() {
747                return Err(crate::Error::EmptyResponseBody(status));
748            }
749            let json_value_response_body: serde_json::Value =
750                serde_json::from_slice(&response_body)?;
751            let json_object_response_body = json_value_response_body.as_object();
752            if let Some(json_object_response_body) = json_object_response_body {
753                let total_count: u64 = json_object_response_body
754                    .get("total_count")
755                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
756                    .as_u64()
757                    .ok_or_else(|| {
758                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
759                    })?;
760                let response_offset: u64 = json_object_response_body
761                    .get("offset")
762                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
763                    .as_u64()
764                    .ok_or_else(|| {
765                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
766                    })?;
767                let response_limit: u64 = json_object_response_body
768                    .get("limit")
769                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
770                    .as_u64()
771                    .ok_or_else(|| {
772                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
773                    })?;
774                let response_wrapper_key = endpoint.response_wrapper_key();
775                let inner_response_body = json_object_response_body
776                    .get(&response_wrapper_key)
777                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
778                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
779                if let Ok(ref parsed_response_body) = result {
780                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
781                }
782                total_results.extend(result?);
783                if total_count < (response_offset + response_limit) {
784                    break;
785                }
786                offset += limit;
787            } else {
788                return Err(crate::Error::NonObjectResponseBody(status));
789            }
790        }
791        Ok(total_results)
792    }
793
794    /// use this to get the results for all pages of a paginated JSON response
795    /// as a Stream
796    pub fn json_response_body_all_pages_stream<E, R>(
797        self: std::sync::Arc<Self>,
798        endpoint: impl EndpointParameter<E>,
799    ) -> AllPagesAsync<E, R>
800    where
801        E: Endpoint + ReturnsJsonResponse + Pageable,
802        R: DeserializeOwned + std::fmt::Debug,
803    {
804        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
805        AllPagesAsync::new(self, endpoint)
806    }
807}
808
809/// A trait representing a parameter value.
810pub trait ParamValue<'a> {
811    #[allow(clippy::wrong_self_convention)]
812    /// The parameter value as a string.
813    fn as_value(&self) -> Cow<'a, str>;
814}
815
816impl ParamValue<'static> for bool {
817    fn as_value(&self) -> Cow<'static, str> {
818        if *self { "true".into() } else { "false".into() }
819    }
820}
821
822impl<'a> ParamValue<'a> for &'a str {
823    fn as_value(&self) -> Cow<'a, str> {
824        (*self).into()
825    }
826}
827
828impl ParamValue<'static> for String {
829    fn as_value(&self) -> Cow<'static, str> {
830        self.clone().into()
831    }
832}
833
834impl<'a> ParamValue<'a> for &'a String {
835    fn as_value(&self) -> Cow<'a, str> {
836        (*self).into()
837    }
838}
839
840/// serialize a [`Vec<T>`] where T implements [ToString] as a string
841/// of comma-separated values
842impl<T> ParamValue<'static> for Vec<T>
843where
844    T: ToString,
845{
846    fn as_value(&self) -> Cow<'static, str> {
847        self.iter()
848            .map(|e| e.to_string())
849            .collect::<Vec<_>>()
850            .join(",")
851            .into()
852    }
853}
854
855/// serialize a [`&Vec<T>`](Vec<T>) where T implements [ToString] as a string
856/// of comma-separated values
857impl<'a, T> ParamValue<'a> for &'a Vec<T>
858where
859    T: ToString,
860{
861    fn as_value(&self) -> Cow<'a, str> {
862        self.iter()
863            .map(|e| e.to_string())
864            .collect::<Vec<_>>()
865            .join(",")
866            .into()
867    }
868}
869
870impl<'a> ParamValue<'a> for Cow<'a, str> {
871    fn as_value(&self) -> Cow<'a, str> {
872        self.clone()
873    }
874}
875
876impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
877    fn as_value(&self) -> Cow<'a, str> {
878        (*self).clone()
879    }
880}
881
882impl ParamValue<'static> for u64 {
883    fn as_value(&self) -> Cow<'static, str> {
884        format!("{self}").into()
885    }
886}
887
888impl ParamValue<'static> for f64 {
889    fn as_value(&self) -> Cow<'static, str> {
890        format!("{self}").into()
891    }
892}
893
894impl ParamValue<'static> for time::OffsetDateTime {
895    fn as_value(&self) -> Cow<'static, str> {
896        self.format(&time::format_description::well_known::Rfc3339)
897            .unwrap()
898            .into()
899    }
900}
901
902impl ParamValue<'static> for time::Date {
903    fn as_value(&self) -> Cow<'static, str> {
904        let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
905        self.format(&format).unwrap().into()
906    }
907}
908
909/// A structure for query parameters.
910#[derive(Debug, Default, Clone)]
911pub struct QueryParams<'a> {
912    /// the actual parameters
913    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
914}
915
916impl<'a> QueryParams<'a> {
917    /// Push a single parameter.
918    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
919    where
920        K: Into<Cow<'a, str>>,
921        V: ParamValue<'b>,
922        'b: 'a,
923    {
924        self.params.push((key.into(), value.as_value()));
925        self
926    }
927
928    /// Push a single parameter.
929    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
930    where
931        K: Into<Cow<'a, str>>,
932        V: ParamValue<'b>,
933        'b: 'a,
934    {
935        if let Some(value) = value {
936            self.params.push((key.into(), value.as_value()));
937        }
938        self
939    }
940
941    /// Push a set of parameters.
942    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
943    where
944        I: Iterator<Item = (K, V)>,
945        K: Into<Cow<'a, str>>,
946        V: ParamValue<'b>,
947        'b: 'a,
948    {
949        self.params
950            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
951        self
952    }
953
954    /// Add the parameters to a URL.
955    pub fn add_to_url(&self, url: &mut Url) {
956        let mut pairs = url.query_pairs_mut();
957        pairs.extend_pairs(self.params.iter());
958    }
959}
960
961/// A trait for providing the necessary information for a single REST API endpoint.
962pub trait Endpoint {
963    /// The HTTP method to use for the endpoint.
964    fn method(&self) -> Method;
965    /// The path to the endpoint.
966    fn endpoint(&self) -> Cow<'static, str>;
967
968    /// Query parameters for the endpoint.
969    fn parameters(&self) -> QueryParams<'_> {
970        QueryParams::default()
971    }
972
973    /// The body for the endpoint.
974    ///
975    /// Returns the `Content-Encoding` header for the data as well as the data itself.
976    ///
977    /// # Errors
978    ///
979    /// The default implementation will never return an error
980    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
981        Ok(None)
982    }
983}
984
985/// A trait to indicate that an endpoint is expected to return a JSON result
986pub trait ReturnsJsonResponse {}
987
988/// A trait to indicate that an endpoint requires pagination to yield all results
989/// or in other words that the non-pagination API should not be used on it or one
990/// might miss some results
991#[diagnostic::on_unimplemented(
992    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)`"
993)]
994pub trait NoPagination {}
995
996/// A trait to indicate that an endpoint is pageable.
997#[diagnostic::on_unimplemented(
998    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)`"
999)]
1000pub trait Pageable {
1001    /// returns the name of the key in the response that contains the list of results
1002    fn response_wrapper_key(&self) -> String;
1003}
1004
1005/// helper to parse created_on and updated_on in the correct format
1006/// (default time serde implementation seems to use a different format)
1007///
1008/// # Errors
1009///
1010/// This will return an error if the underlying string can not be deserialized or
1011/// can not be parsed as an RFC3339 date and time
1012pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
1013where
1014    D: serde::Deserializer<'de>,
1015{
1016    let s = String::deserialize(deserializer)?;
1017
1018    time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1019        .map_err(serde::de::Error::custom)
1020}
1021
1022/// helper to serialize created_on and updated_on in the correct format
1023/// (default time serde implementation seems to use a different format)
1024///
1025/// # Errors
1026///
1027/// This will return an error if the date time can not be formatted as an RFC3339
1028/// date time or the resulting string can not be serialized
1029pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1030where
1031    S: serde::Serializer,
1032{
1033    let s = t
1034        .format(&time::format_description::well_known::Rfc3339)
1035        .map_err(serde::ser::Error::custom)?;
1036
1037    s.serialize(serializer)
1038}
1039
1040/// helper to parse created_on and updated_on in the correct format
1041/// (default time serde implementation seems to use a different format)
1042///
1043/// # Errors
1044///
1045/// This will return an error if the underlying string can not be deserialized
1046/// or it can not be parsed as an RFC3339 date and time
1047pub fn deserialize_optional_rfc3339<'de, D>(
1048    deserializer: D,
1049) -> Result<Option<time::OffsetDateTime>, D::Error>
1050where
1051    D: serde::Deserializer<'de>,
1052{
1053    let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1054
1055    if let Some(s) = s {
1056        Ok(Some(
1057            time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1058                .map_err(serde::de::Error::custom)?,
1059        ))
1060    } else {
1061        Ok(None)
1062    }
1063}
1064
1065/// helper to serialize created_on and updated_on in the correct format
1066/// (default time serde implementation seems to use a different format)
1067///
1068/// # Errors
1069///
1070/// This will return an error if the parameter can not be formatted as RFC3339
1071/// or the resulting string can not be serialized
1072pub fn serialize_optional_rfc3339<S>(
1073    t: &Option<time::OffsetDateTime>,
1074    serializer: S,
1075) -> Result<S::Ok, S::Error>
1076where
1077    S: serde::Serializer,
1078{
1079    if let Some(t) = t {
1080        let s = t
1081            .format(&time::format_description::well_known::Rfc3339)
1082            .map_err(serde::ser::Error::custom)?;
1083
1084        s.serialize(serializer)
1085    } else {
1086        let n: Option<String> = None;
1087        n.serialize(serializer)
1088    }
1089}
1090
1091/// represents an Iterator over all result pages
1092#[derive(Debug)]
1093pub struct AllPages<'i, E, R> {
1094    /// the redmine object to fetch data from
1095    redmine: &'i Redmine,
1096    /// the endpoint to request data from
1097    endpoint: &'i E,
1098    /// the offset to fetch next
1099    offset: u64,
1100    /// the limit for each fetch
1101    limit: u64,
1102    /// the cached total count value from the last request
1103    total_count: Option<u64>,
1104    /// the number of elements already yielded
1105    yielded: u64,
1106    /// the cached values from the last fetch that have not been
1107    /// consumed yet, in reverse order to allow pop to remove them
1108    reversed_rest: Vec<R>,
1109}
1110
1111impl<'i, E, R> AllPages<'i, E, R> {
1112    /// create a new AllPages Iterator
1113    pub fn new(redmine: &'i Redmine, endpoint: &'i E) -> Self {
1114        Self {
1115            redmine,
1116            endpoint,
1117            offset: 0,
1118            limit: 100,
1119            total_count: None,
1120            yielded: 0,
1121            reversed_rest: Vec::new(),
1122        }
1123    }
1124}
1125
1126impl<'i, E, R> Iterator for AllPages<'i, E, R>
1127where
1128    E: Endpoint + ReturnsJsonResponse + Pageable,
1129    R: DeserializeOwned + std::fmt::Debug,
1130{
1131    type Item = Result<R, crate::Error>;
1132
1133    fn next(&mut self) -> Option<Self::Item> {
1134        if let Some(next) = self.reversed_rest.pop() {
1135            self.yielded += 1;
1136            return Some(Ok(next));
1137        }
1138        if let Some(total_count) = self.total_count
1139            && self.offset > total_count
1140        {
1141            return None;
1142        }
1143        match self
1144            .redmine
1145            .json_response_body_page(self.endpoint, self.offset, self.limit)
1146        {
1147            Err(e) => Some(Err(e)),
1148            Ok(ResponsePage {
1149                values,
1150                total_count,
1151                offset,
1152                limit,
1153            }) => {
1154                self.total_count = Some(total_count);
1155                self.offset = offset + limit;
1156                self.reversed_rest = values;
1157                self.reversed_rest.reverse();
1158                if let Some(next) = self.reversed_rest.pop() {
1159                    self.yielded += 1;
1160                    return Some(Ok(next));
1161                }
1162                None
1163            }
1164        }
1165    }
1166
1167    fn size_hint(&self) -> (usize, Option<usize>) {
1168        if let Some(total_count) = self.total_count {
1169            (
1170                self.reversed_rest.len(),
1171                Some((total_count - self.yielded) as usize),
1172            )
1173        } else {
1174            (0, None)
1175        }
1176    }
1177}
1178
1179/// represents an async Stream over all result pages
1180#[pin_project::pin_project]
1181pub struct AllPagesAsync<E, R> {
1182    /// the inner future while we are fetching new data
1183    #[allow(clippy::type_complexity)]
1184    #[pin]
1185    inner: Option<
1186        std::pin::Pin<Box<dyn futures::Future<Output = Result<ResponsePage<R>, crate::Error>>>>,
1187    >,
1188    /// the redmine object to fetch data from
1189    redmine: std::sync::Arc<RedmineAsync>,
1190    /// the endpoint to request data from
1191    endpoint: std::sync::Arc<E>,
1192    /// the offset to fetch next
1193    offset: u64,
1194    /// the limit for each fetch
1195    limit: u64,
1196    /// the cached total count value from the last request
1197    total_count: Option<u64>,
1198    /// the number of elements already yielded
1199    yielded: u64,
1200    /// the cached values from the last fetch that have not been
1201    /// consumed yet, in reverse order to allow pop to remove them
1202    reversed_rest: Vec<R>,
1203}
1204
1205impl<E, R> std::fmt::Debug for AllPagesAsync<E, R>
1206where
1207    R: std::fmt::Debug,
1208{
1209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1210        f.debug_struct("AllPagesAsync")
1211            .field("redmine", &self.redmine)
1212            .field("offset", &self.offset)
1213            .field("limit", &self.limit)
1214            .field("total_count", &self.total_count)
1215            .field("yielded", &self.yielded)
1216            .field("reversed_rest", &self.reversed_rest)
1217            .finish()
1218    }
1219}
1220
1221impl<E, R> AllPagesAsync<E, R> {
1222    /// create a new AllPagesAsync Stream
1223    pub fn new(redmine: std::sync::Arc<RedmineAsync>, endpoint: std::sync::Arc<E>) -> Self {
1224        Self {
1225            inner: None,
1226            redmine,
1227            endpoint,
1228            offset: 0,
1229            limit: 100,
1230            total_count: None,
1231            yielded: 0,
1232            reversed_rest: Vec::new(),
1233        }
1234    }
1235}
1236
1237impl<E, R> futures::stream::Stream for AllPagesAsync<E, R>
1238where
1239    E: Endpoint + ReturnsJsonResponse + Pageable + 'static,
1240    R: DeserializeOwned + std::fmt::Debug + 'static,
1241{
1242    type Item = Result<R, crate::Error>;
1243
1244    fn poll_next(
1245        mut self: std::pin::Pin<&mut Self>,
1246        ctx: &mut std::task::Context<'_>,
1247    ) -> std::task::Poll<Option<Self::Item>> {
1248        if let Some(mut inner) = self.inner.take() {
1249            match inner.as_mut().poll(ctx) {
1250                std::task::Poll::Pending => {
1251                    self.inner = Some(inner);
1252                    std::task::Poll::Pending
1253                }
1254                std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Some(Err(e))),
1255                std::task::Poll::Ready(Ok(ResponsePage {
1256                    values,
1257                    total_count,
1258                    offset,
1259                    limit,
1260                })) => {
1261                    self.total_count = Some(total_count);
1262                    self.offset = offset + limit;
1263                    self.reversed_rest = values;
1264                    self.reversed_rest.reverse();
1265                    if let Some(next) = self.reversed_rest.pop() {
1266                        self.yielded += 1;
1267                        return std::task::Poll::Ready(Some(Ok(next)));
1268                    }
1269                    std::task::Poll::Ready(None)
1270                }
1271            }
1272        } else {
1273            if let Some(next) = self.reversed_rest.pop() {
1274                self.yielded += 1;
1275                return std::task::Poll::Ready(Some(Ok(next)));
1276            }
1277            if let Some(total_count) = self.total_count
1278                && self.offset > total_count
1279            {
1280                return std::task::Poll::Ready(None);
1281            }
1282            self.inner = Some(
1283                self.redmine
1284                    .clone()
1285                    .json_response_body_page(self.endpoint.clone(), self.offset, self.limit)
1286                    .boxed_local(),
1287            );
1288            self.poll_next(ctx)
1289        }
1290    }
1291
1292    fn size_hint(&self) -> (usize, Option<usize>) {
1293        if let Some(total_count) = self.total_count {
1294            (
1295                self.reversed_rest.len(),
1296                Some((total_count - self.yielded) as usize),
1297            )
1298        } else {
1299            (0, None)
1300        }
1301    }
1302}
1303
1304/// trait to allow both `&E` and `std::sync::Arc<E>` as parameters for endpoints
1305/// we can not just use Into because that tries to treat &Endpoint as the value E
1306/// and screws up our other trait bounds
1307///
1308/// if we just used Arc the users would have to change all old call sites
1309pub trait EndpointParameter<E> {
1310    /// convert the endpoint parameter into an Arc
1311    fn into_arc(self) -> std::sync::Arc<E>;
1312}
1313
1314impl<E> EndpointParameter<E> for &E
1315where
1316    E: Clone,
1317{
1318    fn into_arc(self) -> std::sync::Arc<E> {
1319        std::sync::Arc::new(self.to_owned())
1320    }
1321}
1322
1323impl<E> EndpointParameter<E> for std::sync::Arc<E> {
1324    fn into_arc(self) -> std::sync::Arc<E> {
1325        self
1326    }
1327}