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            return Err(crate::Error::HttpErrorResponse(status));
243        } else if status.is_server_error() {
244            error!(%url, %method, "Redmine status error (server error): {:?}", status);
245            return Err(crate::Error::HttpErrorResponse(status));
246        }
247        Ok((status, response_body))
248    }
249
250    /// use this with endpoints that have no response body, e.g. those just deleting
251    /// a Redmine object
252    ///
253    /// # Errors
254    ///
255    /// This can return an error if the endpoint returns an error when creating the request
256    /// body or when the web request fails
257    pub fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
258    where
259        E: Endpoint,
260    {
261        let method = endpoint.method();
262        let url = endpoint.endpoint();
263        let parameters = endpoint.parameters();
264        let mime_type_and_body = endpoint.body()?;
265        self.rest(method, &url, parameters, mime_type_and_body)?;
266        Ok(())
267    }
268
269    /// use this with endpoints which return a JSON response but do not support pagination
270    ///
271    /// # Errors
272    ///
273    /// This can return an error if the endpoint returns an error when creating the request body,
274    /// when the web request fails or when the response can not be parsed as a JSON object
275    /// into the result type
276    pub fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
277    where
278        E: Endpoint + ReturnsJsonResponse + NoPagination,
279        R: DeserializeOwned + std::fmt::Debug,
280    {
281        let method = endpoint.method();
282        let url = endpoint.endpoint();
283        let parameters = endpoint.parameters();
284        let mime_type_and_body = endpoint.body()?;
285        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
286        if response_body.is_empty() {
287            Err(crate::Error::EmptyResponseBody(status))
288        } else {
289            let result = serde_json::from_slice::<R>(&response_body);
290            if let Ok(ref parsed_response_body) = result {
291                trace!("Parsed response body:\n{:#?}", parsed_response_body);
292            }
293            Ok(result?)
294        }
295    }
296
297    /// use this to get a single page of a paginated JSON response
298    /// # Errors
299    ///
300    /// This can return an error if the endpoint returns an error when creating the
301    /// request body, when the web request fails, when the response can not be parsed
302    /// as a JSON object, when any of the pagination keys or the value key are missing
303    /// in the JSON object or when the values can not be parsed as the result type.
304    pub fn json_response_body_page<E, R>(
305        &self,
306        endpoint: &E,
307        offset: u64,
308        limit: u64,
309    ) -> Result<ResponsePage<R>, crate::Error>
310    where
311        E: Endpoint + ReturnsJsonResponse + Pageable,
312        R: DeserializeOwned + std::fmt::Debug,
313    {
314        let method = endpoint.method();
315        let url = endpoint.endpoint();
316        let mut parameters = endpoint.parameters();
317        parameters.push("offset", offset);
318        parameters.push("limit", limit);
319        let mime_type_and_body = endpoint.body()?;
320        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
321        if response_body.is_empty() {
322            Err(crate::Error::EmptyResponseBody(status))
323        } else {
324            let json_value_response_body: serde_json::Value =
325                serde_json::from_slice(&response_body)?;
326            let json_object_response_body = json_value_response_body.as_object();
327            if let Some(json_object_response_body) = json_object_response_body {
328                let total_count = json_object_response_body
329                    .get("total_count")
330                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
331                    .as_u64()
332                    .ok_or_else(|| {
333                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
334                    })?;
335                let offset = json_object_response_body
336                    .get("offset")
337                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
338                    .as_u64()
339                    .ok_or_else(|| {
340                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
341                    })?;
342                let limit = json_object_response_body
343                    .get("limit")
344                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
345                    .as_u64()
346                    .ok_or_else(|| {
347                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
348                    })?;
349                let response_wrapper_key = endpoint.response_wrapper_key();
350                let inner_response_body = json_object_response_body
351                    .get(&response_wrapper_key)
352                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
353                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
354                if let Ok(ref parsed_response_body) = result {
355                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
356                }
357                Ok(ResponsePage {
358                    values: result?,
359                    total_count,
360                    offset,
361                    limit,
362                })
363            } else {
364                Err(crate::Error::NonObjectResponseBody(status))
365            }
366        }
367    }
368
369    /// use this to get the results for all pages of a paginated JSON response
370    ///
371    /// # Errors
372    ///
373    /// This can return an error if the endpoint returns an error when creating the
374    /// request body, when any of the web requests fails, when the response can not be
375    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
376    /// in the JSON object or when the values can not be parsed as the result type.
377    ///
378    pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
379    where
380        E: Endpoint + ReturnsJsonResponse + Pageable,
381        R: DeserializeOwned + std::fmt::Debug,
382    {
383        let method = endpoint.method();
384        let url = endpoint.endpoint();
385        let mut offset = 0;
386        let limit = 100;
387        let mut total_results = vec![];
388        loop {
389            let mut page_parameters = endpoint.parameters();
390            page_parameters.push("offset", offset);
391            page_parameters.push("limit", limit);
392            let mime_type_and_body = endpoint.body()?;
393            let (status, response_body) =
394                self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
395            if response_body.is_empty() {
396                return Err(crate::Error::EmptyResponseBody(status));
397            }
398            let json_value_response_body: serde_json::Value =
399                serde_json::from_slice(&response_body)?;
400            let json_object_response_body = json_value_response_body.as_object();
401            if let Some(json_object_response_body) = json_object_response_body {
402                let total_count: u64 = json_object_response_body
403                    .get("total_count")
404                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
405                    .as_u64()
406                    .ok_or_else(|| {
407                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
408                    })?;
409                let response_offset: u64 = json_object_response_body
410                    .get("offset")
411                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
412                    .as_u64()
413                    .ok_or_else(|| {
414                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
415                    })?;
416                let response_limit: u64 = json_object_response_body
417                    .get("limit")
418                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
419                    .as_u64()
420                    .ok_or_else(|| {
421                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
422                    })?;
423                let response_wrapper_key = endpoint.response_wrapper_key();
424                let inner_response_body = json_object_response_body
425                    .get(&response_wrapper_key)
426                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
427                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
428                if let Ok(ref parsed_response_body) = result {
429                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
430                }
431                total_results.extend(result?);
432                if total_count < (response_offset + response_limit) {
433                    break;
434                }
435                offset += limit;
436            } else {
437                return Err(crate::Error::NonObjectResponseBody(status));
438            }
439        }
440        Ok(total_results)
441    }
442
443    /// use this to get the results for all pages of a paginated JSON response
444    /// as an Iterator
445    pub fn json_response_body_all_pages_iter<'a, 'e, 'i, E, R>(
446        &'a self,
447        endpoint: &'e E,
448    ) -> AllPages<'i, E, R>
449    where
450        E: Endpoint + ReturnsJsonResponse + Pageable,
451        R: DeserializeOwned + std::fmt::Debug,
452        'a: 'i,
453        'e: 'i,
454    {
455        AllPages::new(self, endpoint)
456    }
457}
458
459impl RedmineAsync {
460    /// create a [RedmineAsync] object
461    ///
462    /// # Errors
463    ///
464    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
465    pub fn new(
466        client: reqwest::Client,
467        redmine_url: url::Url,
468        api_key: &str,
469    ) -> Result<std::sync::Arc<Self>, crate::Error> {
470        Ok(std::sync::Arc::new(Self {
471            client,
472            redmine_url,
473            api_key: api_key.to_string(),
474            impersonate_user_id: None,
475        }))
476    }
477
478    /// create a [RedmineAsync] object from the environment variables
479    ///
480    /// REDMINE_API_KEY
481    /// REDMINE_URL
482    ///
483    /// # Errors
484    ///
485    /// This will return an error if the environment variables are
486    /// missing or the URL can not be parsed
487    pub fn from_env(client: reqwest::Client) -> Result<std::sync::Arc<Self>, crate::Error> {
488        let env_options = envy::from_env::<EnvOptions>()?;
489
490        let redmine_url = env_options.redmine_url;
491        let api_key = env_options.redmine_api_key;
492
493        Self::new(client, redmine_url, &api_key)
494    }
495
496    /// Sets the user id of a user to impersonate in all future API calls
497    ///
498    /// this requires Redmine admin privileges
499    pub fn impersonate_user(&mut self, id: u64) {
500        self.impersonate_user_id = Some(id);
501    }
502
503    /// returns the issue URL for a given issue id
504    ///
505    /// this is mostly for convenience since we are already storing the
506    /// redmine URL and it works entirely on the client
507    #[must_use]
508    #[allow(clippy::missing_panics_doc)]
509    pub fn issue_url(&self, issue_id: u64) -> Url {
510        let RedmineAsync { redmine_url, .. } = self;
511        // we can unwrap here because we know /issues/<number>
512        // parses successfully as an url fragment
513        redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
514    }
515
516    /// internal method for shared logic between the methods below which
517    /// diff in how they parse the response body and how often they call this
518    async fn rest(
519        self: std::sync::Arc<Self>,
520        method: reqwest::Method,
521        endpoint: &str,
522        parameters: QueryParams<'_>,
523        mime_type_and_body: Option<(&str, Vec<u8>)>,
524    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
525        let RedmineAsync {
526            client,
527            redmine_url,
528            api_key,
529            impersonate_user_id,
530        } = self.as_ref();
531        let mut url = redmine_url.join(endpoint)?;
532        parameters.add_to_url(&mut url);
533        debug!(%url, %method, "Calling redmine");
534        let req = client
535            .request(method.clone(), url.clone())
536            .header("x-redmine-api-key", api_key);
537        let req = if let Some(user_id) = impersonate_user_id {
538            req.header("X-Redmine-Switch-User", format!("{user_id}"))
539        } else {
540            req
541        };
542        let req = if let Some((mime, data)) = mime_type_and_body {
543            if let Ok(request_body) = from_utf8(&data) {
544                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
545            } else {
546                trace!(
547                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
548                    mime, data
549                );
550            }
551            req.body(data).header("Content-Type", mime)
552        } else {
553            req
554        };
555        let result = req.send().await;
556        if let Err(ref e) = result {
557            error!(%url, %method, "Redmine send error: {:?}", e);
558        }
559        let result = result?;
560        let status = result.status();
561        let response_body = result.bytes().await?;
562        match from_utf8(&response_body) {
563            Ok(response_body) => {
564                trace!("Response body:\n{}", &response_body);
565            }
566            Err(e) => {
567                trace!(
568                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
569                    &e, &response_body
570                );
571            }
572        }
573        if status.is_client_error() {
574            error!(%url, %method, "Redmine status error (client error): {:?}", status);
575        } else if status.is_server_error() {
576            error!(%url, %method, "Redmine status error (server error): {:?}", status);
577        }
578        Ok((status, response_body))
579    }
580
581    /// use this with endpoints that have no response body, e.g. those just deleting
582    /// a Redmine object
583    ///
584    /// # Errors
585    ///
586    /// This can return an error if the endpoint returns an error when creating the request
587    /// body or when the web request fails
588    pub async fn ignore_response_body<E>(
589        self: std::sync::Arc<Self>,
590        endpoint: impl EndpointParameter<E>,
591    ) -> Result<(), crate::Error>
592    where
593        E: Endpoint,
594    {
595        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
596        let method = endpoint.method();
597        let url = endpoint.endpoint();
598        let parameters = endpoint.parameters();
599        let mime_type_and_body = endpoint.body()?;
600        self.rest(method, &url, parameters, mime_type_and_body)
601            .await?;
602        Ok(())
603    }
604
605    /// use this with endpoints which return a JSON response but do not support pagination
606    ///
607    /// you can use it with those that support pagination but they will only return the first page
608    ///
609    /// # Errors
610    ///
611    /// This can return an error if the endpoint returns an error when creating the request body,
612    /// when the web request fails or when the response can not be parsed as a JSON object
613    /// into the result type
614    pub async fn json_response_body<E, R>(
615        self: std::sync::Arc<Self>,
616        endpoint: impl EndpointParameter<E>,
617    ) -> Result<R, crate::Error>
618    where
619        E: Endpoint + ReturnsJsonResponse + NoPagination,
620        R: DeserializeOwned + std::fmt::Debug,
621    {
622        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
623        let method = endpoint.method();
624        let url = endpoint.endpoint();
625        let parameters = endpoint.parameters();
626        let mime_type_and_body = endpoint.body()?;
627        let (status, response_body) = self
628            .rest(method, &url, parameters, mime_type_and_body)
629            .await?;
630        if response_body.is_empty() {
631            Err(crate::Error::EmptyResponseBody(status))
632        } else {
633            let result = serde_json::from_slice::<R>(&response_body);
634            if let Ok(ref parsed_response_body) = result {
635                trace!("Parsed response body:\n{:#?}", parsed_response_body);
636            }
637            Ok(result?)
638        }
639    }
640
641    /// use this to get a single page of a paginated JSON response
642    /// # Errors
643    ///
644    /// This can return an error if the endpoint returns an error when creating the
645    /// request body, when the web request fails, when the response can not be parsed
646    /// as a JSON object, when any of the pagination keys or the value key are missing
647    /// in the JSON object or when the values can not be parsed as the result type.
648    pub async fn json_response_body_page<E, R>(
649        self: std::sync::Arc<Self>,
650        endpoint: impl EndpointParameter<E>,
651        offset: u64,
652        limit: u64,
653    ) -> Result<ResponsePage<R>, crate::Error>
654    where
655        E: Endpoint + ReturnsJsonResponse + Pageable,
656        R: DeserializeOwned + std::fmt::Debug,
657    {
658        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
659        let method = endpoint.method();
660        let url = endpoint.endpoint();
661        let mut parameters = endpoint.parameters();
662        parameters.push("offset", offset);
663        parameters.push("limit", limit);
664        let mime_type_and_body = endpoint.body()?;
665        let (status, response_body) = self
666            .rest(method, &url, parameters, mime_type_and_body)
667            .await?;
668        if response_body.is_empty() {
669            Err(crate::Error::EmptyResponseBody(status))
670        } else {
671            let json_value_response_body: serde_json::Value =
672                serde_json::from_slice(&response_body)?;
673            let json_object_response_body = json_value_response_body.as_object();
674            if let Some(json_object_response_body) = json_object_response_body {
675                let total_count = json_object_response_body
676                    .get("total_count")
677                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
678                    .as_u64()
679                    .ok_or_else(|| {
680                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
681                    })?;
682                let offset = json_object_response_body
683                    .get("offset")
684                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
685                    .as_u64()
686                    .ok_or_else(|| {
687                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
688                    })?;
689                let limit = json_object_response_body
690                    .get("limit")
691                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
692                    .as_u64()
693                    .ok_or_else(|| {
694                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
695                    })?;
696                let response_wrapper_key = endpoint.response_wrapper_key();
697                let inner_response_body = json_object_response_body
698                    .get(&response_wrapper_key)
699                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
700                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
701                if let Ok(ref parsed_response_body) = result {
702                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
703                }
704                Ok(ResponsePage {
705                    values: result?,
706                    total_count,
707                    offset,
708                    limit,
709                })
710            } else {
711                Err(crate::Error::NonObjectResponseBody(status))
712            }
713        }
714    }
715
716    /// use this to get the results for all pages of a paginated JSON response
717    ///
718    /// # Errors
719    ///
720    /// This can return an error if the endpoint returns an error when creating the
721    /// request body, when any of the web requests fails, when the response can not be
722    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
723    /// in the JSON object or when the values can not be parsed as the result type.
724    ///
725    pub async fn json_response_body_all_pages<E, R>(
726        self: std::sync::Arc<Self>,
727        endpoint: impl EndpointParameter<E>,
728    ) -> Result<Vec<R>, crate::Error>
729    where
730        E: Endpoint + ReturnsJsonResponse + Pageable,
731        R: DeserializeOwned + std::fmt::Debug,
732    {
733        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
734        let method = endpoint.method();
735        let url = endpoint.endpoint();
736        let mut offset = 0;
737        let limit = 100;
738        let mut total_results = vec![];
739        loop {
740            let mut page_parameters = endpoint.parameters();
741            page_parameters.push("offset", offset);
742            page_parameters.push("limit", limit);
743            let mime_type_and_body = endpoint.body()?;
744            let (status, response_body) = self
745                .clone()
746                .rest(method.clone(), &url, page_parameters, mime_type_and_body)
747                .await?;
748            if response_body.is_empty() {
749                return Err(crate::Error::EmptyResponseBody(status));
750            }
751            let json_value_response_body: serde_json::Value =
752                serde_json::from_slice(&response_body)?;
753            let json_object_response_body = json_value_response_body.as_object();
754            if let Some(json_object_response_body) = json_object_response_body {
755                let total_count: u64 = json_object_response_body
756                    .get("total_count")
757                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
758                    .as_u64()
759                    .ok_or_else(|| {
760                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
761                    })?;
762                let response_offset: u64 = json_object_response_body
763                    .get("offset")
764                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
765                    .as_u64()
766                    .ok_or_else(|| {
767                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
768                    })?;
769                let response_limit: u64 = json_object_response_body
770                    .get("limit")
771                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
772                    .as_u64()
773                    .ok_or_else(|| {
774                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
775                    })?;
776                let response_wrapper_key = endpoint.response_wrapper_key();
777                let inner_response_body = json_object_response_body
778                    .get(&response_wrapper_key)
779                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
780                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
781                if let Ok(ref parsed_response_body) = result {
782                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
783                }
784                total_results.extend(result?);
785                if total_count < (response_offset + response_limit) {
786                    break;
787                }
788                offset += limit;
789            } else {
790                return Err(crate::Error::NonObjectResponseBody(status));
791            }
792        }
793        Ok(total_results)
794    }
795
796    /// use this to get the results for all pages of a paginated JSON response
797    /// as a Stream
798    pub fn json_response_body_all_pages_stream<E, R>(
799        self: std::sync::Arc<Self>,
800        endpoint: impl EndpointParameter<E>,
801    ) -> AllPagesAsync<E, R>
802    where
803        E: Endpoint + ReturnsJsonResponse + Pageable,
804        R: DeserializeOwned + std::fmt::Debug,
805    {
806        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
807        AllPagesAsync::new(self, endpoint)
808    }
809}
810
811/// A trait representing a parameter value.
812pub trait ParamValue<'a> {
813    #[allow(clippy::wrong_self_convention)]
814    /// The parameter value as a string.
815    fn as_value(&self) -> Cow<'a, str>;
816}
817
818impl ParamValue<'static> for bool {
819    fn as_value(&self) -> Cow<'static, str> {
820        if *self { "true".into() } else { "false".into() }
821    }
822}
823
824impl<'a> ParamValue<'a> for &'a str {
825    fn as_value(&self) -> Cow<'a, str> {
826        (*self).into()
827    }
828}
829
830impl ParamValue<'static> for String {
831    fn as_value(&self) -> Cow<'static, str> {
832        self.clone().into()
833    }
834}
835
836impl<'a> ParamValue<'a> for &'a String {
837    fn as_value(&self) -> Cow<'a, str> {
838        (*self).into()
839    }
840}
841
842/// serialize a [`Vec<T>`] where T implements [ToString] as a string
843/// of comma-separated values
844impl<T> ParamValue<'static> for Vec<T>
845where
846    T: ToString,
847{
848    fn as_value(&self) -> Cow<'static, str> {
849        self.iter()
850            .map(|e| e.to_string())
851            .collect::<Vec<_>>()
852            .join(",")
853            .into()
854    }
855}
856
857/// serialize a [`&Vec<T>`](Vec<T>) where T implements [ToString] as a string
858/// of comma-separated values
859impl<'a, T> ParamValue<'a> for &'a Vec<T>
860where
861    T: ToString,
862{
863    fn as_value(&self) -> Cow<'a, str> {
864        self.iter()
865            .map(|e| e.to_string())
866            .collect::<Vec<_>>()
867            .join(",")
868            .into()
869    }
870}
871
872impl<'a> ParamValue<'a> for Cow<'a, str> {
873    fn as_value(&self) -> Cow<'a, str> {
874        self.clone()
875    }
876}
877
878impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
879    fn as_value(&self) -> Cow<'a, str> {
880        (*self).clone()
881    }
882}
883
884impl ParamValue<'static> for u64 {
885    fn as_value(&self) -> Cow<'static, str> {
886        format!("{self}").into()
887    }
888}
889
890impl ParamValue<'static> for f64 {
891    fn as_value(&self) -> Cow<'static, str> {
892        format!("{self}").into()
893    }
894}
895
896impl ParamValue<'static> for time::OffsetDateTime {
897    fn as_value(&self) -> Cow<'static, str> {
898        self.format(&time::format_description::well_known::Rfc3339)
899            .unwrap()
900            .into()
901    }
902}
903
904impl ParamValue<'static> for time::Date {
905    fn as_value(&self) -> Cow<'static, str> {
906        let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
907        self.format(&format).unwrap().into()
908    }
909}
910
911/// A structure for query parameters.
912#[derive(Debug, Default, Clone)]
913pub struct QueryParams<'a> {
914    /// the actual parameters
915    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
916}
917
918impl<'a> QueryParams<'a> {
919    /// Push a single parameter.
920    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
921    where
922        K: Into<Cow<'a, str>>,
923        V: ParamValue<'b>,
924        'b: 'a,
925    {
926        self.params.push((key.into(), value.as_value()));
927        self
928    }
929
930    /// Push a single parameter.
931    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
932    where
933        K: Into<Cow<'a, str>>,
934        V: ParamValue<'b>,
935        'b: 'a,
936    {
937        if let Some(value) = value {
938            self.params.push((key.into(), value.as_value()));
939        }
940        self
941    }
942
943    /// Push a set of parameters.
944    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
945    where
946        I: Iterator<Item = (K, V)>,
947        K: Into<Cow<'a, str>>,
948        V: ParamValue<'b>,
949        'b: 'a,
950    {
951        self.params
952            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
953        self
954    }
955
956    /// Add the parameters to a URL.
957    pub fn add_to_url(&self, url: &mut Url) {
958        let mut pairs = url.query_pairs_mut();
959        pairs.extend_pairs(self.params.iter());
960    }
961}
962
963/// A trait for providing the necessary information for a single REST API endpoint.
964pub trait Endpoint {
965    /// The HTTP method to use for the endpoint.
966    fn method(&self) -> Method;
967    /// The path to the endpoint.
968    fn endpoint(&self) -> Cow<'static, str>;
969
970    /// Query parameters for the endpoint.
971    fn parameters(&self) -> QueryParams<'_> {
972        QueryParams::default()
973    }
974
975    /// The body for the endpoint.
976    ///
977    /// Returns the `Content-Encoding` header for the data as well as the data itself.
978    ///
979    /// # Errors
980    ///
981    /// The default implementation will never return an error
982    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
983        Ok(None)
984    }
985}
986
987/// A trait to indicate that an endpoint is expected to return a JSON result
988pub trait ReturnsJsonResponse {}
989
990/// A trait to indicate that an endpoint requires pagination to yield all results
991/// or in other words that the non-pagination API should not be used on it or one
992/// might miss some results
993#[diagnostic::on_unimplemented(
994    message = "{Self} is an endpoint that either returns nothing or requires pagination, use `.ignore_response_body(&endpoint)`, `.json_response_body_page(&endpoint, offset, limit)` or `.json_response_body_all_pages(&endpoint)` instead of `.json_response_body(&endpoint)`"
995)]
996pub trait NoPagination {}
997
998/// A trait to indicate that an endpoint is pageable.
999#[diagnostic::on_unimplemented(
1000    message = "{Self} is an endpoint that does not implement pagination or returns nothing, use `.ignore_response_body(&endpoint)` or `.json_response_body(&endpoint)` instead of `.json_response_body_page(&endpoint, offset, limit)` or `.json_response_body_all_pages(&endpoint)`"
1001)]
1002pub trait Pageable {
1003    /// returns the name of the key in the response that contains the list of results
1004    fn response_wrapper_key(&self) -> String;
1005}
1006
1007/// helper to parse created_on and updated_on in the correct format
1008/// (default time serde implementation seems to use a different format)
1009///
1010/// # Errors
1011///
1012/// This will return an error if the underlying string can not be deserialized or
1013/// can not be parsed as an RFC3339 date and time
1014pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
1015where
1016    D: serde::Deserializer<'de>,
1017{
1018    let s = String::deserialize(deserializer)?;
1019
1020    time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1021        .map_err(serde::de::Error::custom)
1022}
1023
1024/// helper to serialize created_on and updated_on in the correct format
1025/// (default time serde implementation seems to use a different format)
1026///
1027/// # Errors
1028///
1029/// This will return an error if the date time can not be formatted as an RFC3339
1030/// date time or the resulting string can not be serialized
1031pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1032where
1033    S: serde::Serializer,
1034{
1035    let s = t
1036        .format(&time::format_description::well_known::Rfc3339)
1037        .map_err(serde::ser::Error::custom)?;
1038
1039    s.serialize(serializer)
1040}
1041
1042/// helper to parse created_on and updated_on in the correct format
1043/// (default time serde implementation seems to use a different format)
1044///
1045/// # Errors
1046///
1047/// This will return an error if the underlying string can not be deserialized
1048/// or it can not be parsed as an RFC3339 date and time
1049pub fn deserialize_optional_rfc3339<'de, D>(
1050    deserializer: D,
1051) -> Result<Option<time::OffsetDateTime>, D::Error>
1052where
1053    D: serde::Deserializer<'de>,
1054{
1055    let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1056
1057    if let Some(s) = s {
1058        Ok(Some(
1059            time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1060                .map_err(serde::de::Error::custom)?,
1061        ))
1062    } else {
1063        Ok(None)
1064    }
1065}
1066
1067/// helper to serialize created_on and updated_on in the correct format
1068/// (default time serde implementation seems to use a different format)
1069///
1070/// # Errors
1071///
1072/// This will return an error if the parameter can not be formatted as RFC3339
1073/// or the resulting string can not be serialized
1074pub fn serialize_optional_rfc3339<S>(
1075    t: &Option<time::OffsetDateTime>,
1076    serializer: S,
1077) -> Result<S::Ok, S::Error>
1078where
1079    S: serde::Serializer,
1080{
1081    if let Some(t) = t {
1082        let s = t
1083            .format(&time::format_description::well_known::Rfc3339)
1084            .map_err(serde::ser::Error::custom)?;
1085
1086        s.serialize(serializer)
1087    } else {
1088        let n: Option<String> = None;
1089        n.serialize(serializer)
1090    }
1091}
1092
1093/// represents an Iterator over all result pages
1094#[derive(Debug)]
1095pub struct AllPages<'i, E, R> {
1096    /// the redmine object to fetch data from
1097    redmine: &'i Redmine,
1098    /// the endpoint to request data from
1099    endpoint: &'i E,
1100    /// the offset to fetch next
1101    offset: u64,
1102    /// the limit for each fetch
1103    limit: u64,
1104    /// the cached total count value from the last request
1105    total_count: Option<u64>,
1106    /// the number of elements already yielded
1107    yielded: u64,
1108    /// the cached values from the last fetch that have not been
1109    /// consumed yet, in reverse order to allow pop to remove them
1110    reversed_rest: Vec<R>,
1111}
1112
1113impl<'i, E, R> AllPages<'i, E, R> {
1114    /// create a new AllPages Iterator
1115    pub fn new(redmine: &'i Redmine, endpoint: &'i E) -> Self {
1116        Self {
1117            redmine,
1118            endpoint,
1119            offset: 0,
1120            limit: 100,
1121            total_count: None,
1122            yielded: 0,
1123            reversed_rest: Vec::new(),
1124        }
1125    }
1126}
1127
1128impl<'i, E, R> Iterator for AllPages<'i, E, R>
1129where
1130    E: Endpoint + ReturnsJsonResponse + Pageable,
1131    R: DeserializeOwned + std::fmt::Debug,
1132{
1133    type Item = Result<R, crate::Error>;
1134
1135    fn next(&mut self) -> Option<Self::Item> {
1136        if let Some(next) = self.reversed_rest.pop() {
1137            self.yielded += 1;
1138            return Some(Ok(next));
1139        }
1140        if let Some(total_count) = self.total_count
1141            && self.offset > total_count
1142        {
1143            return None;
1144        }
1145        match self
1146            .redmine
1147            .json_response_body_page(self.endpoint, self.offset, self.limit)
1148        {
1149            Err(e) => Some(Err(e)),
1150            Ok(ResponsePage {
1151                values,
1152                total_count,
1153                offset,
1154                limit,
1155            }) => {
1156                self.total_count = Some(total_count);
1157                self.offset = offset + limit;
1158                self.reversed_rest = values;
1159                self.reversed_rest.reverse();
1160                if let Some(next) = self.reversed_rest.pop() {
1161                    self.yielded += 1;
1162                    return Some(Ok(next));
1163                }
1164                None
1165            }
1166        }
1167    }
1168
1169    fn size_hint(&self) -> (usize, Option<usize>) {
1170        if let Some(total_count) = self.total_count {
1171            (
1172                self.reversed_rest.len(),
1173                Some((total_count - self.yielded) as usize),
1174            )
1175        } else {
1176            (0, None)
1177        }
1178    }
1179}
1180
1181/// represents an async Stream over all result pages
1182#[pin_project::pin_project]
1183pub struct AllPagesAsync<E, R> {
1184    /// the inner future while we are fetching new data
1185    #[allow(clippy::type_complexity)]
1186    #[pin]
1187    inner: Option<
1188        std::pin::Pin<Box<dyn futures::Future<Output = Result<ResponsePage<R>, crate::Error>>>>,
1189    >,
1190    /// the redmine object to fetch data from
1191    redmine: std::sync::Arc<RedmineAsync>,
1192    /// the endpoint to request data from
1193    endpoint: std::sync::Arc<E>,
1194    /// the offset to fetch next
1195    offset: u64,
1196    /// the limit for each fetch
1197    limit: u64,
1198    /// the cached total count value from the last request
1199    total_count: Option<u64>,
1200    /// the number of elements already yielded
1201    yielded: u64,
1202    /// the cached values from the last fetch that have not been
1203    /// consumed yet, in reverse order to allow pop to remove them
1204    reversed_rest: Vec<R>,
1205}
1206
1207impl<E, R> std::fmt::Debug for AllPagesAsync<E, R>
1208where
1209    R: std::fmt::Debug,
1210{
1211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212        f.debug_struct("AllPagesAsync")
1213            .field("redmine", &self.redmine)
1214            .field("offset", &self.offset)
1215            .field("limit", &self.limit)
1216            .field("total_count", &self.total_count)
1217            .field("yielded", &self.yielded)
1218            .field("reversed_rest", &self.reversed_rest)
1219            .finish()
1220    }
1221}
1222
1223impl<E, R> AllPagesAsync<E, R> {
1224    /// create a new AllPagesAsync Stream
1225    pub fn new(redmine: std::sync::Arc<RedmineAsync>, endpoint: std::sync::Arc<E>) -> Self {
1226        Self {
1227            inner: None,
1228            redmine,
1229            endpoint,
1230            offset: 0,
1231            limit: 100,
1232            total_count: None,
1233            yielded: 0,
1234            reversed_rest: Vec::new(),
1235        }
1236    }
1237}
1238
1239impl<E, R> futures::stream::Stream for AllPagesAsync<E, R>
1240where
1241    E: Endpoint + ReturnsJsonResponse + Pageable + 'static,
1242    R: DeserializeOwned + std::fmt::Debug + 'static,
1243{
1244    type Item = Result<R, crate::Error>;
1245
1246    fn poll_next(
1247        mut self: std::pin::Pin<&mut Self>,
1248        ctx: &mut std::task::Context<'_>,
1249    ) -> std::task::Poll<Option<Self::Item>> {
1250        if let Some(mut inner) = self.inner.take() {
1251            match inner.as_mut().poll(ctx) {
1252                std::task::Poll::Pending => {
1253                    self.inner = Some(inner);
1254                    std::task::Poll::Pending
1255                }
1256                std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Some(Err(e))),
1257                std::task::Poll::Ready(Ok(ResponsePage {
1258                    values,
1259                    total_count,
1260                    offset,
1261                    limit,
1262                })) => {
1263                    self.total_count = Some(total_count);
1264                    self.offset = offset + limit;
1265                    self.reversed_rest = values;
1266                    self.reversed_rest.reverse();
1267                    if let Some(next) = self.reversed_rest.pop() {
1268                        self.yielded += 1;
1269                        return std::task::Poll::Ready(Some(Ok(next)));
1270                    }
1271                    std::task::Poll::Ready(None)
1272                }
1273            }
1274        } else {
1275            if let Some(next) = self.reversed_rest.pop() {
1276                self.yielded += 1;
1277                return std::task::Poll::Ready(Some(Ok(next)));
1278            }
1279            if let Some(total_count) = self.total_count
1280                && self.offset > total_count
1281            {
1282                return std::task::Poll::Ready(None);
1283            }
1284            self.inner = Some(
1285                self.redmine
1286                    .clone()
1287                    .json_response_body_page(self.endpoint.clone(), self.offset, self.limit)
1288                    .boxed_local(),
1289            );
1290            self.poll_next(ctx)
1291        }
1292    }
1293
1294    fn size_hint(&self) -> (usize, Option<usize>) {
1295        if let Some(total_count) = self.total_count {
1296            (
1297                self.reversed_rest.len(),
1298                Some((total_count - self.yielded) as usize),
1299            )
1300        } else {
1301            (0, None)
1302        }
1303    }
1304}
1305
1306/// trait to allow both `&E` and `std::sync::Arc<E>` as parameters for endpoints
1307/// we can not just use Into because that tries to treat &Endpoint as the value E
1308/// and screws up our other trait bounds
1309///
1310/// if we just used Arc the users would have to change all old call sites
1311pub trait EndpointParameter<E> {
1312    /// convert the endpoint parameter into an Arc
1313    fn into_arc(self) -> std::sync::Arc<E>;
1314}
1315
1316impl<E> EndpointParameter<E> for &E
1317where
1318    E: Clone,
1319{
1320    fn into_arc(self) -> std::sync::Arc<E> {
1321        std::sync::Arc::new(self.to_owned())
1322    }
1323}
1324
1325impl<E> EndpointParameter<E> for std::sync::Arc<E> {
1326    fn into_arc(self) -> std::sync::Arc<E> {
1327        self
1328    }
1329}