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 redmine base url
171    #[must_use]
172    pub fn redmine_url(&self) -> &Url {
173        &self.redmine_url
174    }
175
176    /// returns the issue URL for a given issue id
177    ///
178    /// this is mostly for convenience since we are already storing the
179    /// redmine URL and it works entirely on the client
180    #[must_use]
181    #[allow(clippy::missing_panics_doc)]
182    pub fn issue_url(&self, issue_id: u64) -> Url {
183        let Redmine { redmine_url, .. } = self;
184        // we can unwrap here because we know /issues/<number>
185        // parses successfully as an url fragment
186        redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
187    }
188
189    /// internal method for shared logic between the methods below which
190    /// diff in how they parse the response body and how often they call this
191    fn rest(
192        &self,
193        method: reqwest::Method,
194        endpoint: &str,
195        parameters: QueryParams,
196        mime_type_and_body: Option<(&str, Vec<u8>)>,
197    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
198        let Redmine {
199            client,
200            redmine_url,
201            api_key,
202            impersonate_user_id,
203        } = self;
204        let mut url = redmine_url.join(endpoint)?;
205        parameters.add_to_url(&mut url);
206        debug!(%url, %method, "Calling redmine");
207        let req = client
208            .request(method.clone(), url.clone())
209            .header("x-redmine-api-key", api_key);
210        let req = if let Some(user_id) = impersonate_user_id {
211            req.header("X-Redmine-Switch-User", format!("{user_id}"))
212        } else {
213            req
214        };
215        let req = if let Some((mime, data)) = mime_type_and_body {
216            if let Ok(request_body) = from_utf8(&data) {
217                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
218            } else {
219                trace!(
220                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
221                    mime, data
222                );
223            }
224            req.body(data).header("Content-Type", mime)
225        } else {
226            req
227        };
228        let result = req.send();
229        if let Err(ref e) = result {
230            error!(%url, %method, "Redmine send error: {:?}", e);
231        }
232        let result = result?;
233        let status = result.status();
234        let response_body = result.bytes()?;
235        match from_utf8(&response_body) {
236            Ok(response_body) => {
237                trace!("Response body:\n{}", &response_body);
238            }
239            Err(e) => {
240                trace!(
241                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
242                    &e, &response_body
243                );
244            }
245        }
246        if status.is_client_error() {
247            error!(%url, %method, "Redmine status error (client error): {:?} response: {:?}", status, from_utf8(&response_body));
248            return Err(crate::Error::HttpErrorResponse(status));
249        } else if status.is_server_error() {
250            error!(%url, %method, "Redmine status error (server error): {:?} response: {:?}", status, from_utf8(&response_body));
251            return Err(crate::Error::HttpErrorResponse(status));
252        }
253        Ok((status, response_body))
254    }
255
256    /// use this with endpoints that have no response body, e.g. those just deleting
257    /// a Redmine object
258    ///
259    /// # Errors
260    ///
261    /// This can return an error if the endpoint returns an error when creating the request
262    /// body or when the web request fails
263    pub fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
264    where
265        E: Endpoint,
266    {
267        let method = endpoint.method();
268        let url = endpoint.endpoint();
269        let parameters = endpoint.parameters();
270        let mime_type_and_body = endpoint.body()?;
271        self.rest(method, &url, parameters, mime_type_and_body)?;
272        Ok(())
273    }
274
275    /// use this with endpoints which return a JSON response but do not support pagination
276    ///
277    /// # Errors
278    ///
279    /// This can return an error if the endpoint returns an error when creating the request body,
280    /// when the web request fails or when the response can not be parsed as a JSON object
281    /// into the result type
282    pub fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
283    where
284        E: Endpoint + ReturnsJsonResponse + NoPagination,
285        R: DeserializeOwned + std::fmt::Debug,
286    {
287        let method = endpoint.method();
288        let url = endpoint.endpoint();
289        let parameters = endpoint.parameters();
290        let mime_type_and_body = endpoint.body()?;
291        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
292        if response_body.is_empty() {
293            Err(crate::Error::EmptyResponseBody(status))
294        } else {
295            let result = serde_json::from_slice::<R>(&response_body);
296            if let Ok(ref parsed_response_body) = result {
297                trace!("Parsed response body:\n{:#?}", parsed_response_body);
298            }
299            Ok(result?)
300        }
301    }
302
303    /// use this to get a single page of a paginated JSON response
304    /// # Errors
305    ///
306    /// This can return an error if the endpoint returns an error when creating the
307    /// request body, when the web request fails, when the response can not be parsed
308    /// as a JSON object, when any of the pagination keys or the value key are missing
309    /// in the JSON object or when the values can not be parsed as the result type.
310    pub fn json_response_body_page<E, R>(
311        &self,
312        endpoint: &E,
313        offset: u64,
314        limit: u64,
315    ) -> Result<ResponsePage<R>, crate::Error>
316    where
317        E: Endpoint + ReturnsJsonResponse + Pageable,
318        R: DeserializeOwned + std::fmt::Debug,
319    {
320        let method = endpoint.method();
321        let url = endpoint.endpoint();
322        let mut parameters = endpoint.parameters();
323        parameters.push("offset", offset);
324        parameters.push("limit", limit);
325        let mime_type_and_body = endpoint.body()?;
326        let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
327        if response_body.is_empty() {
328            Err(crate::Error::EmptyResponseBody(status))
329        } else {
330            let json_value_response_body: serde_json::Value =
331                serde_json::from_slice(&response_body)?;
332            let json_object_response_body = json_value_response_body.as_object();
333            if let Some(json_object_response_body) = json_object_response_body {
334                let total_count = json_object_response_body
335                    .get("total_count")
336                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
337                    .as_u64()
338                    .ok_or_else(|| {
339                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
340                    })?;
341                let offset = json_object_response_body
342                    .get("offset")
343                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
344                    .as_u64()
345                    .ok_or_else(|| {
346                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
347                    })?;
348                let limit = json_object_response_body
349                    .get("limit")
350                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
351                    .as_u64()
352                    .ok_or_else(|| {
353                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
354                    })?;
355                let response_wrapper_key = endpoint.response_wrapper_key();
356                let inner_response_body = json_object_response_body
357                    .get(&response_wrapper_key)
358                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
359                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
360                if let Ok(ref parsed_response_body) = result {
361                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
362                }
363                Ok(ResponsePage {
364                    values: result?,
365                    total_count,
366                    offset,
367                    limit,
368                })
369            } else {
370                Err(crate::Error::NonObjectResponseBody(status))
371            }
372        }
373    }
374
375    /// use this to get the results for all pages of a paginated JSON response
376    ///
377    /// # Errors
378    ///
379    /// This can return an error if the endpoint returns an error when creating the
380    /// request body, when any of the web requests fails, when the response can not be
381    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
382    /// in the JSON object or when the values can not be parsed as the result type.
383    ///
384    pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
385    where
386        E: Endpoint + ReturnsJsonResponse + Pageable,
387        R: DeserializeOwned + std::fmt::Debug,
388    {
389        let method = endpoint.method();
390        let url = endpoint.endpoint();
391        let mut offset = 0;
392        let limit = 100;
393        let mut total_results = vec![];
394        loop {
395            let mut page_parameters = endpoint.parameters();
396            page_parameters.push("offset", offset);
397            page_parameters.push("limit", limit);
398            let mime_type_and_body = endpoint.body()?;
399            let (status, response_body) =
400                self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
401            if response_body.is_empty() {
402                return Err(crate::Error::EmptyResponseBody(status));
403            }
404            let json_value_response_body: serde_json::Value =
405                serde_json::from_slice(&response_body)?;
406            let json_object_response_body = json_value_response_body.as_object();
407            if let Some(json_object_response_body) = json_object_response_body {
408                let total_count: u64 = json_object_response_body
409                    .get("total_count")
410                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
411                    .as_u64()
412                    .ok_or_else(|| {
413                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
414                    })?;
415                let response_offset: u64 = json_object_response_body
416                    .get("offset")
417                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
418                    .as_u64()
419                    .ok_or_else(|| {
420                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
421                    })?;
422                let response_limit: u64 = json_object_response_body
423                    .get("limit")
424                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
425                    .as_u64()
426                    .ok_or_else(|| {
427                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
428                    })?;
429                let response_wrapper_key = endpoint.response_wrapper_key();
430                let inner_response_body = json_object_response_body
431                    .get(&response_wrapper_key)
432                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
433                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
434                if let Ok(ref parsed_response_body) = result {
435                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
436                }
437                total_results.extend(result?);
438                if total_count < (response_offset + response_limit) {
439                    break;
440                }
441                offset += limit;
442            } else {
443                return Err(crate::Error::NonObjectResponseBody(status));
444            }
445        }
446        Ok(total_results)
447    }
448
449    /// use this to get the results for all pages of a paginated JSON response
450    /// as an Iterator
451    pub fn json_response_body_all_pages_iter<'a, 'e, 'i, E, R>(
452        &'a self,
453        endpoint: &'e E,
454    ) -> AllPages<'i, E, R>
455    where
456        E: Endpoint + ReturnsJsonResponse + Pageable,
457        R: DeserializeOwned + std::fmt::Debug,
458        'a: 'i,
459        'e: 'i,
460    {
461        AllPages::new(self, endpoint)
462    }
463}
464
465impl RedmineAsync {
466    /// create a [RedmineAsync] object
467    ///
468    /// # Errors
469    ///
470    /// This will return [`crate::Error::ReqwestError`] if initialization of Reqwest client is failed.
471    pub fn new(
472        client: reqwest::Client,
473        redmine_url: url::Url,
474        api_key: &str,
475    ) -> Result<std::sync::Arc<Self>, crate::Error> {
476        Ok(std::sync::Arc::new(Self {
477            client,
478            redmine_url,
479            api_key: api_key.to_string(),
480            impersonate_user_id: None,
481        }))
482    }
483
484    /// create a [RedmineAsync] object from the environment variables
485    ///
486    /// REDMINE_API_KEY
487    /// REDMINE_URL
488    ///
489    /// # Errors
490    ///
491    /// This will return an error if the environment variables are
492    /// missing or the URL can not be parsed
493    pub fn from_env(client: reqwest::Client) -> Result<std::sync::Arc<Self>, crate::Error> {
494        let env_options = envy::from_env::<EnvOptions>()?;
495
496        let redmine_url = env_options.redmine_url;
497        let api_key = env_options.redmine_api_key;
498
499        Self::new(client, redmine_url, &api_key)
500    }
501
502    /// Sets the user id of a user to impersonate in all future API calls
503    ///
504    /// this requires Redmine admin privileges
505    pub fn impersonate_user(&mut self, id: u64) {
506        self.impersonate_user_id = Some(id);
507    }
508
509    /// returns the redmine base url
510    #[must_use]
511    pub fn redmine_url(&self) -> &Url {
512        &self.redmine_url
513    }
514
515    /// returns the issue URL for a given issue id
516    ///
517    /// this is mostly for convenience since we are already storing the
518    /// redmine URL and it works entirely on the client
519    #[must_use]
520    #[allow(clippy::missing_panics_doc)]
521    pub fn issue_url(&self, issue_id: u64) -> Url {
522        let RedmineAsync { redmine_url, .. } = self;
523        // we can unwrap here because we know /issues/<number>
524        // parses successfully as an url fragment
525        redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
526    }
527
528    /// internal method for shared logic between the methods below which
529    /// diff in how they parse the response body and how often they call this
530    async fn rest(
531        self: std::sync::Arc<Self>,
532        method: reqwest::Method,
533        endpoint: &str,
534        parameters: QueryParams<'_>,
535        mime_type_and_body: Option<(&str, Vec<u8>)>,
536    ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
537        let RedmineAsync {
538            client,
539            redmine_url,
540            api_key,
541            impersonate_user_id,
542        } = self.as_ref();
543        let mut url = redmine_url.join(endpoint)?;
544        parameters.add_to_url(&mut url);
545        debug!(%url, %method, "Calling redmine");
546        let req = client
547            .request(method.clone(), url.clone())
548            .header("x-redmine-api-key", api_key);
549        let req = if let Some(user_id) = impersonate_user_id {
550            req.header("X-Redmine-Switch-User", format!("{user_id}"))
551        } else {
552            req
553        };
554        let req = if let Some((mime, data)) = mime_type_and_body {
555            if let Ok(request_body) = from_utf8(&data) {
556                trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
557            } else {
558                trace!(
559                    "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
560                    mime, data
561                );
562            }
563            req.body(data).header("Content-Type", mime)
564        } else {
565            req
566        };
567        let result = req.send().await;
568        if let Err(ref e) = result {
569            error!(%url, %method, "Redmine send error: {:?}", e);
570        }
571        let result = result?;
572        let status = result.status();
573        let response_body = result.bytes().await?;
574        match from_utf8(&response_body) {
575            Ok(response_body) => {
576                trace!("Response body:\n{}", &response_body);
577            }
578            Err(e) => {
579                trace!(
580                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
581                    &e, &response_body
582                );
583            }
584        }
585        if status.is_client_error() {
586            error!(%url, %method, "Redmine status error (client error): {:?} response: {:?}", status, from_utf8(&response_body));
587        } else if status.is_server_error() {
588            error!(%url, %method, "Redmine status error (server error): {:?} response: {:?}", status, from_utf8(&response_body));
589        }
590        Ok((status, response_body))
591    }
592
593    /// use this with endpoints that have no response body, e.g. those just deleting
594    /// a Redmine object
595    ///
596    /// # Errors
597    ///
598    /// This can return an error if the endpoint returns an error when creating the request
599    /// body or when the web request fails
600    pub async fn ignore_response_body<E>(
601        self: std::sync::Arc<Self>,
602        endpoint: impl EndpointParameter<E>,
603    ) -> Result<(), crate::Error>
604    where
605        E: Endpoint,
606    {
607        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
608        let method = endpoint.method();
609        let url = endpoint.endpoint();
610        let parameters = endpoint.parameters();
611        let mime_type_and_body = endpoint.body()?;
612        self.rest(method, &url, parameters, mime_type_and_body)
613            .await?;
614        Ok(())
615    }
616
617    /// use this with endpoints which return a JSON response but do not support pagination
618    ///
619    /// you can use it with those that support pagination but they will only return the first page
620    ///
621    /// # Errors
622    ///
623    /// This can return an error if the endpoint returns an error when creating the request body,
624    /// when the web request fails or when the response can not be parsed as a JSON object
625    /// into the result type
626    pub async fn json_response_body<E, R>(
627        self: std::sync::Arc<Self>,
628        endpoint: impl EndpointParameter<E>,
629    ) -> Result<R, crate::Error>
630    where
631        E: Endpoint + ReturnsJsonResponse + NoPagination,
632        R: DeserializeOwned + std::fmt::Debug,
633    {
634        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
635        let method = endpoint.method();
636        let url = endpoint.endpoint();
637        let parameters = endpoint.parameters();
638        let mime_type_and_body = endpoint.body()?;
639        let (status, response_body) = self
640            .rest(method, &url, parameters, mime_type_and_body)
641            .await?;
642        if response_body.is_empty() {
643            Err(crate::Error::EmptyResponseBody(status))
644        } else {
645            let result = serde_json::from_slice::<R>(&response_body);
646            if let Ok(ref parsed_response_body) = result {
647                trace!("Parsed response body:\n{:#?}", parsed_response_body);
648            }
649            Ok(result?)
650        }
651    }
652
653    /// use this to get a single page of a paginated JSON response
654    /// # Errors
655    ///
656    /// This can return an error if the endpoint returns an error when creating the
657    /// request body, when the web request fails, when the response can not be parsed
658    /// as a JSON object, when any of the pagination keys or the value key are missing
659    /// in the JSON object or when the values can not be parsed as the result type.
660    pub async fn json_response_body_page<E, R>(
661        self: std::sync::Arc<Self>,
662        endpoint: impl EndpointParameter<E>,
663        offset: u64,
664        limit: u64,
665    ) -> Result<ResponsePage<R>, crate::Error>
666    where
667        E: Endpoint + ReturnsJsonResponse + Pageable,
668        R: DeserializeOwned + std::fmt::Debug,
669    {
670        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
671        let method = endpoint.method();
672        let url = endpoint.endpoint();
673        let mut parameters = endpoint.parameters();
674        parameters.push("offset", offset);
675        parameters.push("limit", limit);
676        let mime_type_and_body = endpoint.body()?;
677        let (status, response_body) = self
678            .rest(method, &url, parameters, mime_type_and_body)
679            .await?;
680        if response_body.is_empty() {
681            Err(crate::Error::EmptyResponseBody(status))
682        } else {
683            let json_value_response_body: serde_json::Value =
684                serde_json::from_slice(&response_body)?;
685            let json_object_response_body = json_value_response_body.as_object();
686            if let Some(json_object_response_body) = json_object_response_body {
687                let total_count = json_object_response_body
688                    .get("total_count")
689                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
690                    .as_u64()
691                    .ok_or_else(|| {
692                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
693                    })?;
694                let offset = json_object_response_body
695                    .get("offset")
696                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
697                    .as_u64()
698                    .ok_or_else(|| {
699                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
700                    })?;
701                let limit = json_object_response_body
702                    .get("limit")
703                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
704                    .as_u64()
705                    .ok_or_else(|| {
706                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
707                    })?;
708                let response_wrapper_key = endpoint.response_wrapper_key();
709                let inner_response_body = json_object_response_body
710                    .get(&response_wrapper_key)
711                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
712                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
713                if let Ok(ref parsed_response_body) = result {
714                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
715                }
716                Ok(ResponsePage {
717                    values: result?,
718                    total_count,
719                    offset,
720                    limit,
721                })
722            } else {
723                Err(crate::Error::NonObjectResponseBody(status))
724            }
725        }
726    }
727
728    /// use this to get the results for all pages of a paginated JSON response
729    ///
730    /// # Errors
731    ///
732    /// This can return an error if the endpoint returns an error when creating the
733    /// request body, when any of the web requests fails, when the response can not be
734    /// parsed as a JSON object, when any of the pagination keys or the value key are missing
735    /// in the JSON object or when the values can not be parsed as the result type.
736    ///
737    pub async fn json_response_body_all_pages<E, R>(
738        self: std::sync::Arc<Self>,
739        endpoint: impl EndpointParameter<E>,
740    ) -> Result<Vec<R>, crate::Error>
741    where
742        E: Endpoint + ReturnsJsonResponse + Pageable,
743        R: DeserializeOwned + std::fmt::Debug,
744    {
745        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
746        let method = endpoint.method();
747        let url = endpoint.endpoint();
748        let mut offset = 0;
749        let limit = 100;
750        let mut total_results = vec![];
751        loop {
752            let mut page_parameters = endpoint.parameters();
753            page_parameters.push("offset", offset);
754            page_parameters.push("limit", limit);
755            let mime_type_and_body = endpoint.body()?;
756            let (status, response_body) = self
757                .clone()
758                .rest(method.clone(), &url, page_parameters, mime_type_and_body)
759                .await?;
760            if response_body.is_empty() {
761                return Err(crate::Error::EmptyResponseBody(status));
762            }
763            let json_value_response_body: serde_json::Value =
764                serde_json::from_slice(&response_body)?;
765            let json_object_response_body = json_value_response_body.as_object();
766            if let Some(json_object_response_body) = json_object_response_body {
767                let total_count: u64 = json_object_response_body
768                    .get("total_count")
769                    .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
770                    .as_u64()
771                    .ok_or_else(|| {
772                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
773                    })?;
774                let response_offset: u64 = json_object_response_body
775                    .get("offset")
776                    .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
777                    .as_u64()
778                    .ok_or_else(|| {
779                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
780                    })?;
781                let response_limit: u64 = json_object_response_body
782                    .get("limit")
783                    .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
784                    .as_u64()
785                    .ok_or_else(|| {
786                        crate::Error::PaginationKeyHasWrongType("total_count".to_string())
787                    })?;
788                let response_wrapper_key = endpoint.response_wrapper_key();
789                let inner_response_body = json_object_response_body
790                    .get(&response_wrapper_key)
791                    .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
792                let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
793                if let Ok(ref parsed_response_body) = result {
794                    trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
795                }
796                total_results.extend(result?);
797                if total_count < (response_offset + response_limit) {
798                    break;
799                }
800                offset += limit;
801            } else {
802                return Err(crate::Error::NonObjectResponseBody(status));
803            }
804        }
805        Ok(total_results)
806    }
807
808    /// use this to get the results for all pages of a paginated JSON response
809    /// as a Stream
810    pub fn json_response_body_all_pages_stream<E, R>(
811        self: std::sync::Arc<Self>,
812        endpoint: impl EndpointParameter<E>,
813    ) -> AllPagesAsync<E, R>
814    where
815        E: Endpoint + ReturnsJsonResponse + Pageable,
816        R: DeserializeOwned + std::fmt::Debug,
817    {
818        let endpoint: std::sync::Arc<E> = endpoint.into_arc();
819        AllPagesAsync::new(self, endpoint)
820    }
821}
822
823/// A trait representing a parameter value.
824pub trait ParamValue<'a> {
825    #[allow(clippy::wrong_self_convention)]
826    /// The parameter value as a string.
827    fn as_value(&self) -> Cow<'a, str>;
828}
829
830impl ParamValue<'static> for bool {
831    fn as_value(&self) -> Cow<'static, str> {
832        if *self { "true".into() } else { "false".into() }
833    }
834}
835
836impl<'a> ParamValue<'a> for &'a str {
837    fn as_value(&self) -> Cow<'a, str> {
838        (*self).into()
839    }
840}
841
842impl ParamValue<'static> for String {
843    fn as_value(&self) -> Cow<'static, str> {
844        self.clone().into()
845    }
846}
847
848impl<'a> ParamValue<'a> for &'a String {
849    fn as_value(&self) -> Cow<'a, str> {
850        (*self).into()
851    }
852}
853
854/// serialize a [`Vec<T>`] where T implements [ToString] as a string
855/// of comma-separated values
856impl<T> ParamValue<'static> for Vec<T>
857where
858    T: ToString,
859{
860    fn as_value(&self) -> Cow<'static, str> {
861        self.iter()
862            .map(|e| e.to_string())
863            .collect::<Vec<_>>()
864            .join(",")
865            .into()
866    }
867}
868
869/// serialize a [`&Vec<T>`](Vec<T>) where T implements [ToString] as a string
870/// of comma-separated values
871impl<'a, T> ParamValue<'a> for &'a Vec<T>
872where
873    T: ToString,
874{
875    fn as_value(&self) -> Cow<'a, str> {
876        self.iter()
877            .map(|e| e.to_string())
878            .collect::<Vec<_>>()
879            .join(",")
880            .into()
881    }
882}
883
884impl<'a> ParamValue<'a> for Cow<'a, str> {
885    fn as_value(&self) -> Cow<'a, str> {
886        self.clone()
887    }
888}
889
890impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
891    fn as_value(&self) -> Cow<'a, str> {
892        (*self).clone()
893    }
894}
895
896impl ParamValue<'static> for u64 {
897    fn as_value(&self) -> Cow<'static, str> {
898        format!("{self}").into()
899    }
900}
901
902impl ParamValue<'static> for f64 {
903    fn as_value(&self) -> Cow<'static, str> {
904        format!("{self}").into()
905    }
906}
907
908impl ParamValue<'static> for time::OffsetDateTime {
909    fn as_value(&self) -> Cow<'static, str> {
910        self.format(&time::format_description::well_known::Rfc3339)
911            .unwrap()
912            .into()
913    }
914}
915
916impl ParamValue<'static> for time::Date {
917    fn as_value(&self) -> Cow<'static, str> {
918        let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
919        self.format(&format).unwrap().into()
920    }
921}
922
923/// Filter for a comparable date time filters for past
924/// used for filters on created_on, updated_on fields
925#[derive(Debug, Clone)]
926pub enum DateTimeFilterPast {
927    /// an exact match
928    ExactMatch(time::OffsetDateTime),
929    /// a range match (inclusive)
930    Range(time::OffsetDateTime, time::OffsetDateTime),
931    /// we only want values less than or equal to the parameter
932    LessThanOrEqual(time::OffsetDateTime),
933    /// we only want values greater than or equal to the parameter
934    GreaterThanOrEqual(time::OffsetDateTime),
935    /// less than n days ago
936    LessThanDaysAgo(u32),
937    /// more than n days ago
938    MoreThanDaysAgo(u32),
939    /// within the past n days
940    WithinPastDays(u32),
941    /// exactly n days ago
942    ExactDaysAgo(u32),
943    /// today
944    Today,
945    /// yesterday
946    Yesterday,
947    /// this week
948    ThisWeek,
949    /// last week
950    LastWeek,
951    /// last 2 weeks
952    LastTwoWeeks,
953    /// this month
954    ThisMonth,
955    /// last month
956    LastMonth,
957    /// this year
958    ThisYear,
959    /// unset value (NULL in DB)
960    Unset,
961    /// any value (NOT NULL in DB)
962    Any,
963}
964
965impl std::fmt::Display for DateTimeFilterPast {
966    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
967        let format =
968            time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z");
969        match self {
970            DateTimeFilterPast::ExactMatch(v) => {
971                write!(
972                    f,
973                    "{}",
974                    v.format(&format).expect(
975                        "Error formatting OffsetDateTime in DateTimeFilterPast::ExactMatch"
976                    )
977                )
978            }
979            DateTimeFilterPast::Range(v_start, v_end) => {
980                write!(
981                    f,
982                    "><{}|{}",
983                    v_start.format(&format).expect(
984                        "Error formatting first OffsetDateTime in DateTimeFilterPast::Range"
985                    ),
986                    v_end.format(&format).expect(
987                        "Error formatting second OffsetDateTime in DateTimeFilterPast::Range"
988                    ),
989                )
990            }
991            DateTimeFilterPast::LessThanOrEqual(v) => {
992                write!(
993                    f,
994                    "<={}",
995                    v.format(&format).expect(
996                        "Error formatting OffsetDateTime in DateTimeFilterPast::LessThanOrEqual"
997                    )
998                )
999            }
1000            DateTimeFilterPast::GreaterThanOrEqual(v) => {
1001                write!(
1002                    f,
1003                    ">={}",
1004                    v.format(&format).expect(
1005                        "Error formatting OffsetDateTime in DateTimeFilterPast::GreaterThanOrEqual"
1006                    )
1007                )
1008            }
1009            DateTimeFilterPast::LessThanDaysAgo(d) => {
1010                write!(f, ">t-{}", d)
1011            }
1012            DateTimeFilterPast::MoreThanDaysAgo(d) => {
1013                write!(f, "<t-{}", d)
1014            }
1015            DateTimeFilterPast::WithinPastDays(d) => {
1016                write!(f, "><t-{}", d)
1017            }
1018            DateTimeFilterPast::ExactDaysAgo(d) => {
1019                write!(f, "t-{}", d)
1020            }
1021            DateTimeFilterPast::Today => {
1022                write!(f, "t")
1023            }
1024            DateTimeFilterPast::Yesterday => {
1025                write!(f, "ld")
1026            }
1027            DateTimeFilterPast::ThisWeek => {
1028                write!(f, "w")
1029            }
1030            DateTimeFilterPast::LastWeek => {
1031                write!(f, "lw")
1032            }
1033            DateTimeFilterPast::LastTwoWeeks => {
1034                write!(f, "l2w")
1035            }
1036            DateTimeFilterPast::ThisMonth => {
1037                write!(f, "m")
1038            }
1039            DateTimeFilterPast::LastMonth => {
1040                write!(f, "lm")
1041            }
1042            DateTimeFilterPast::ThisYear => {
1043                write!(f, "y")
1044            }
1045            DateTimeFilterPast::Unset => {
1046                write!(f, "!*")
1047            }
1048            DateTimeFilterPast::Any => {
1049                write!(f, "*")
1050            }
1051        }
1052    }
1053}
1054
1055/// Filter options for subject and description
1056#[derive(Debug, Clone)]
1057pub enum StringFieldFilter {
1058    /// match exactly this value
1059    ExactMatch(String),
1060    /// match this substring of the actual value
1061    SubStringMatch(String),
1062}
1063
1064impl std::fmt::Display for StringFieldFilter {
1065    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1066        match self {
1067            StringFieldFilter::ExactMatch(s) => {
1068                write!(f, "{s}")
1069            }
1070            StringFieldFilter::SubStringMatch(s) => {
1071                write!(f, "~{s}")
1072            }
1073        }
1074    }
1075}
1076
1077/// A filter for a custom field, consisting of its ID and a StringFieldFilter for its value.
1078#[derive(Debug, Clone)]
1079pub struct CustomFieldFilter {
1080    /// The ID of the custom field to filter by.
1081    pub id: u64,
1082    /// The value to filter the custom field by, using a `StringFieldFilter`.
1083    pub value: StringFieldFilter,
1084}
1085
1086/// Filter for float values, supporting various comparison operators.
1087#[derive(Debug, Clone)]
1088pub enum FloatFilter {
1089    /// An exact match for the float value.
1090    ExactMatch(f64),
1091    /// A range match (inclusive) for two float values.
1092    Range(f64, f64),
1093    /// Values less than or equal to the specified float.
1094    LessThanOrEqual(f64),
1095    /// Values greater than or equal to the specified float.
1096    GreaterThanOrEqual(f64),
1097    /// Any value (equivalent to `> 0`).
1098    Any,
1099    /// No value (equivalent to `= 0`).
1100    None,
1101}
1102
1103impl std::fmt::Display for FloatFilter {
1104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1105        match self {
1106            FloatFilter::ExactMatch(v) => write!(f, "{}", v),
1107            FloatFilter::Range(v_start, v_end) => write!(f, "><{}|{}", v_start, v_end),
1108            FloatFilter::LessThanOrEqual(v) => write!(f, "<={}", v),
1109            FloatFilter::GreaterThanOrEqual(v) => write!(f, ">={}", v),
1110            FloatFilter::Any => write!(f, "*"),
1111            FloatFilter::None => write!(f, "!*"),
1112        }
1113    }
1114}
1115
1116/// Filter for integer values, supporting various comparison operators.
1117#[derive(Debug, Clone)]
1118pub enum IntegerFilter {
1119    /// An exact match for the integer value.
1120    ExactMatch(u64),
1121    /// A range match (inclusive) for two integer values.
1122    Range(u64, u64),
1123    /// Values less than or equal to the specified integer.
1124    LessThanOrEqual(u64),
1125    /// Values greater than or equal to the specified integer.
1126    GreaterThanOrEqual(u64),
1127    /// Any value (equivalent to `> 0`).
1128    Any,
1129    /// No value (equivalent to `= 0`).
1130    None,
1131}
1132
1133impl std::fmt::Display for IntegerFilter {
1134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1135        match self {
1136            IntegerFilter::ExactMatch(v) => write!(f, "{}", v),
1137            IntegerFilter::Range(v_start, v_end) => write!(f, "><{}|{}", v_start, v_end),
1138            IntegerFilter::LessThanOrEqual(v) => write!(f, "<={}", v),
1139            IntegerFilter::GreaterThanOrEqual(v) => write!(f, ">={}", v),
1140            IntegerFilter::Any => write!(f, "*"),
1141            IntegerFilter::None => write!(f, "!*"),
1142        }
1143    }
1144}
1145
1146/// Filter for tracker IDs.
1147#[derive(Debug, Clone)]
1148pub enum TrackerFilter {
1149    /// Match any tracker.
1150    Any,
1151    /// Match no tracker.
1152    None,
1153    /// Match a specific list of trackers.
1154    TheseTrackers(Vec<u64>),
1155    /// Match any tracker but a specific list of trackers.
1156    NotTheseTrackers(Vec<u64>),
1157}
1158
1159impl std::fmt::Display for TrackerFilter {
1160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1161        match self {
1162            TrackerFilter::Any => write!(f, "*"),
1163            TrackerFilter::None => write!(f, "!*"),
1164            TrackerFilter::TheseTrackers(ids) => {
1165                let s: String = ids
1166                    .iter()
1167                    .map(|e| e.to_string())
1168                    .collect::<Vec<_>>()
1169                    .join(",");
1170                write!(f, "{s}")
1171            }
1172            TrackerFilter::NotTheseTrackers(ids) => {
1173                let s: String = ids
1174                    .iter()
1175                    .map(|e| format!("!{e}"))
1176                    .collect::<Vec<_>>()
1177                    .join(",");
1178                write!(f, "{s}")
1179            }
1180        }
1181    }
1182}
1183
1184/// Filter for activity IDs.
1185#[derive(Debug, Clone)]
1186pub enum ActivityFilter {
1187    /// Match any activity.
1188    Any,
1189    /// Match no activity.
1190    None,
1191    /// Match a specific list of activities.
1192    TheseActivities(Vec<u64>),
1193    /// Match any activity but a specific list of activities.
1194    NotTheseActivities(Vec<u64>),
1195}
1196
1197impl std::fmt::Display for ActivityFilter {
1198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199        match self {
1200            ActivityFilter::Any => write!(f, "*"),
1201            ActivityFilter::None => write!(f, "!*"),
1202            ActivityFilter::TheseActivities(ids) => {
1203                let s: String = ids
1204                    .iter()
1205                    .map(|e| e.to_string())
1206                    .collect::<Vec<_>>()
1207                    .join(",");
1208                write!(f, "{s}")
1209            }
1210            ActivityFilter::NotTheseActivities(ids) => {
1211                let s: String = ids
1212                    .iter()
1213                    .map(|e| format!("!{e}"))
1214                    .collect::<Vec<_>>()
1215                    .join(",");
1216                write!(f, "{s}")
1217            }
1218        }
1219    }
1220}
1221
1222/// Filter for fixed version IDs.
1223#[derive(Debug, Clone)]
1224pub enum VersionFilter {
1225    /// Match any version.
1226    Any,
1227    /// Match no version.
1228    None,
1229    /// Match a specific list of versions.
1230    TheseVersions(Vec<u64>),
1231    /// Match any version but a specific list of versions.
1232    NotTheseVersions(Vec<u64>),
1233}
1234
1235impl std::fmt::Display for VersionFilter {
1236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1237        match self {
1238            VersionFilter::Any => write!(f, "*"),
1239            VersionFilter::None => write!(f, "!*"),
1240            VersionFilter::TheseVersions(ids) => {
1241                let s: String = ids
1242                    .iter()
1243                    .map(|e| e.to_string())
1244                    .collect::<Vec<_>>()
1245                    .join(",");
1246                write!(f, "{s}")
1247            }
1248            VersionFilter::NotTheseVersions(ids) => {
1249                let s: String = ids
1250                    .iter()
1251                    .map(|e| format!("!{e}"))
1252                    .collect::<Vec<_>>()
1253                    .join(",");
1254                write!(f, "{s}")
1255            }
1256        }
1257    }
1258}
1259
1260/// Filter for date values, supporting various comparison operators.
1261#[derive(Debug, Clone)]
1262pub enum DateFilter {
1263    /// an exact match
1264    ExactMatch(time::Date),
1265    /// a range match (inclusive)
1266    Range(time::Date, time::Date),
1267    /// we only want values less than or equal to the parameter
1268    LessThanOrEqual(time::Date),
1269    /// we only want values greater than or equal to the parameter
1270    GreaterThanOrEqual(time::Date),
1271    /// less than n days ago
1272    LessThanDaysAgo(u32),
1273    /// more than n days ago
1274    MoreThanDaysAgo(u32),
1275    /// within the past n days
1276    WithinPastDays(u32),
1277    /// exactly n days ago
1278    ExactDaysAgo(u32),
1279    /// in less than n days
1280    InLessThanDays(u32),
1281    /// in more than n days
1282    InMoreThanDays(u32),
1283    /// in the next n days
1284    WithinFutureDays(u32),
1285    /// in exactly n days
1286    InExactDays(u32),
1287    /// today
1288    Today,
1289    /// yesterday
1290    Yesterday,
1291    /// tomorrow
1292    Tomorrow,
1293    /// this week
1294    ThisWeek,
1295    /// last week
1296    LastWeek,
1297    /// last 2 weeks
1298    LastTwoWeeks,
1299    /// next week
1300    NextWeek,
1301    /// this month
1302    ThisMonth,
1303    /// last month
1304    LastMonth,
1305    /// next month
1306    NextMonth,
1307    /// this year
1308    ThisYear,
1309    /// unset value (NULL in DB)
1310    Unset,
1311    /// any value (NOT NULL in DB)
1312    Any,
1313}
1314
1315impl std::fmt::Display for DateFilter {
1316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1317        let format = time::macros::format_description!("[year]-[month]-[day]");
1318        match self {
1319            DateFilter::ExactMatch(v) => {
1320                write!(
1321                    f,
1322                    "{}",
1323                    v.format(&format)
1324                        .expect("Error formatting Date in DateFilter::ExactMatch")
1325                )
1326            }
1327            DateFilter::Range(v_start, v_end) => {
1328                write!(
1329                    f,
1330                    "><{}|{}",
1331                    v_start
1332                        .format(&format)
1333                        .expect("Error formatting first Date in DateFilter::Range"),
1334                    v_end
1335                        .format(&format)
1336                        .expect("Error formatting second Date in DateFilter::Range"),
1337                )
1338            }
1339            DateFilter::LessThanOrEqual(v) => {
1340                write!(
1341                    f,
1342                    "<={}",
1343                    v.format(&format)
1344                        .expect("Error formatting Date in DateFilter::LessThanOrEqual")
1345                )
1346            }
1347            DateFilter::GreaterThanOrEqual(v) => {
1348                write!(
1349                    f,
1350                    ">={}",
1351                    v.format(&format)
1352                        .expect("Error formatting Date in DateFilter::GreaterThanOrEqual")
1353                )
1354            }
1355            DateFilter::LessThanDaysAgo(d) => {
1356                write!(f, ">t-{}", d)
1357            }
1358            DateFilter::MoreThanDaysAgo(d) => {
1359                write!(f, "<t-{}", d)
1360            }
1361            DateFilter::WithinPastDays(d) => {
1362                write!(f, "><t-{}", d)
1363            }
1364            DateFilter::ExactDaysAgo(d) => {
1365                write!(f, "t-{}", d)
1366            }
1367            DateFilter::InLessThanDays(d) => {
1368                write!(f, "<t+{}", d)
1369            }
1370            DateFilter::InMoreThanDays(d) => {
1371                write!(f, ">t+{}", d)
1372            }
1373            DateFilter::WithinFutureDays(d) => {
1374                write!(f, "><t+{}", d)
1375            }
1376            DateFilter::InExactDays(d) => {
1377                write!(f, "t+{}", d)
1378            }
1379            DateFilter::Today => {
1380                write!(f, "t")
1381            }
1382            DateFilter::Yesterday => {
1383                write!(f, "ld")
1384            }
1385            DateFilter::Tomorrow => {
1386                write!(f, "nd")
1387            }
1388            DateFilter::ThisWeek => {
1389                write!(f, "w")
1390            }
1391            DateFilter::LastWeek => {
1392                write!(f, "lw")
1393            }
1394            DateFilter::LastTwoWeeks => {
1395                write!(f, "l2w")
1396            }
1397            DateFilter::NextWeek => {
1398                write!(f, "nw")
1399            }
1400            DateFilter::ThisMonth => {
1401                write!(f, "m")
1402            }
1403            DateFilter::LastMonth => {
1404                write!(f, "lm")
1405            }
1406            DateFilter::NextMonth => {
1407                write!(f, "nm")
1408            }
1409            DateFilter::ThisYear => {
1410                write!(f, "y")
1411            }
1412            DateFilter::Unset => {
1413                write!(f, "!*")
1414            }
1415            DateFilter::Any => {
1416                write!(f, "*")
1417            }
1418        }
1419    }
1420}
1421
1422/// A structure for query parameters.
1423#[derive(Debug, Default, Clone)]
1424pub struct QueryParams<'a> {
1425    /// the actual parameters
1426    params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
1427}
1428
1429impl<'a> QueryParams<'a> {
1430    /// Push a single parameter.
1431    pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
1432    where
1433        K: Into<Cow<'a, str>>,
1434        V: ParamValue<'b>,
1435        'b: 'a,
1436    {
1437        self.params.push((key.into(), value.as_value()));
1438        self
1439    }
1440
1441    /// Push a single parameter.
1442    pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
1443    where
1444        K: Into<Cow<'a, str>>,
1445        V: ParamValue<'b>,
1446        'b: 'a,
1447    {
1448        if let Some(value) = value {
1449            self.params.push((key.into(), value.as_value()));
1450        }
1451        self
1452    }
1453
1454    /// Push a set of parameters.
1455    pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
1456    where
1457        I: Iterator<Item = (K, V)>,
1458        K: Into<Cow<'a, str>>,
1459        V: ParamValue<'b>,
1460        'b: 'a,
1461    {
1462        self.params
1463            .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
1464        self
1465    }
1466
1467    /// Add the parameters to a URL.
1468    pub fn add_to_url(&self, url: &mut Url) {
1469        let mut pairs = url.query_pairs_mut();
1470        pairs.extend_pairs(self.params.iter());
1471    }
1472}
1473
1474/// A trait for providing the necessary information for a single REST API endpoint.
1475pub trait Endpoint {
1476    /// The HTTP method to use for the endpoint.
1477    fn method(&self) -> Method;
1478    /// The path to the endpoint.
1479    fn endpoint(&self) -> Cow<'static, str>;
1480
1481    /// Query parameters for the endpoint.
1482    fn parameters(&self) -> QueryParams<'_> {
1483        QueryParams::default()
1484    }
1485
1486    /// The body for the endpoint.
1487    ///
1488    /// Returns the `Content-Encoding` header for the data as well as the data itself.
1489    ///
1490    /// # Errors
1491    ///
1492    /// The default implementation will never return an error
1493    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1494        Ok(None)
1495    }
1496}
1497
1498/// A trait to indicate that an endpoint is expected to return a JSON result
1499pub trait ReturnsJsonResponse {}
1500
1501/// A trait to indicate that an endpoint requires pagination to yield all results
1502/// or in other words that the non-pagination API should not be used on it or one
1503/// might miss some results
1504#[diagnostic::on_unimplemented(
1505    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)`"
1506)]
1507pub trait NoPagination {}
1508
1509/// A trait to indicate that an endpoint is pageable.
1510#[diagnostic::on_unimplemented(
1511    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)`"
1512)]
1513pub trait Pageable {
1514    /// returns the name of the key in the response that contains the list of results
1515    fn response_wrapper_key(&self) -> String;
1516}
1517
1518/// helper to parse created_on and updated_on in the correct format
1519/// (default time serde implementation seems to use a different format)
1520///
1521/// # Errors
1522///
1523/// This will return an error if the underlying string can not be deserialized or
1524/// can not be parsed as an RFC3339 date and time
1525pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
1526where
1527    D: serde::Deserializer<'de>,
1528{
1529    let s = String::deserialize(deserializer)?;
1530
1531    time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1532        .map_err(serde::de::Error::custom)
1533}
1534
1535/// helper to serialize created_on and updated_on in the correct format
1536/// (default time serde implementation seems to use a different format)
1537///
1538/// # Errors
1539///
1540/// This will return an error if the date time can not be formatted as an RFC3339
1541/// date time or the resulting string can not be serialized
1542pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1543where
1544    S: serde::Serializer,
1545{
1546    let s = t
1547        .format(&time::format_description::well_known::Rfc3339)
1548        .map_err(serde::ser::Error::custom)?;
1549
1550    s.serialize(serializer)
1551}
1552
1553/// helper to parse created_on and updated_on in the correct format
1554/// (default time serde implementation seems to use a different format)
1555///
1556/// # Errors
1557///
1558/// This will return an error if the underlying string can not be deserialized
1559/// or it can not be parsed as an RFC3339 date and time
1560pub fn deserialize_optional_rfc3339<'de, D>(
1561    deserializer: D,
1562) -> Result<Option<time::OffsetDateTime>, D::Error>
1563where
1564    D: serde::Deserializer<'de>,
1565{
1566    let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1567
1568    if let Some(s) = s {
1569        Ok(Some(
1570            time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1571                .map_err(serde::de::Error::custom)?,
1572        ))
1573    } else {
1574        Ok(None)
1575    }
1576}
1577
1578/// helper to serialize created_on and updated_on in the correct format
1579/// (default time serde implementation seems to use a different format)
1580///
1581/// # Errors
1582///
1583/// This will return an error if the parameter can not be formatted as RFC3339
1584/// or the resulting string can not be serialized
1585pub fn serialize_optional_rfc3339<S>(
1586    t: &Option<time::OffsetDateTime>,
1587    serializer: S,
1588) -> Result<S::Ok, S::Error>
1589where
1590    S: serde::Serializer,
1591{
1592    if let Some(t) = t {
1593        let s = t
1594            .format(&time::format_description::well_known::Rfc3339)
1595            .map_err(serde::ser::Error::custom)?;
1596
1597        s.serialize(serializer)
1598    } else {
1599        let n: Option<String> = None;
1600        n.serialize(serializer)
1601    }
1602}
1603
1604/// represents an Iterator over all result pages
1605#[derive(Debug)]
1606pub struct AllPages<'i, E, R> {
1607    /// the redmine object to fetch data from
1608    redmine: &'i Redmine,
1609    /// the endpoint to request data from
1610    endpoint: &'i E,
1611    /// the offset to fetch next
1612    offset: u64,
1613    /// the limit for each fetch
1614    limit: u64,
1615    /// the cached total count value from the last request
1616    total_count: Option<u64>,
1617    /// the number of elements already yielded
1618    yielded: u64,
1619    /// the cached values from the last fetch that have not been
1620    /// consumed yet, in reverse order to allow pop to remove them
1621    reversed_rest: Vec<R>,
1622}
1623
1624impl<'i, E, R> AllPages<'i, E, R> {
1625    /// create a new AllPages Iterator
1626    pub fn new(redmine: &'i Redmine, endpoint: &'i E) -> Self {
1627        Self {
1628            redmine,
1629            endpoint,
1630            offset: 0,
1631            limit: 100,
1632            total_count: None,
1633            yielded: 0,
1634            reversed_rest: Vec::new(),
1635        }
1636    }
1637}
1638
1639impl<'i, E, R> Iterator for AllPages<'i, E, R>
1640where
1641    E: Endpoint + ReturnsJsonResponse + Pageable,
1642    R: DeserializeOwned + std::fmt::Debug,
1643{
1644    type Item = Result<R, crate::Error>;
1645
1646    fn next(&mut self) -> Option<Self::Item> {
1647        if let Some(next) = self.reversed_rest.pop() {
1648            self.yielded += 1;
1649            return Some(Ok(next));
1650        }
1651        if let Some(total_count) = self.total_count
1652            && self.offset > total_count
1653        {
1654            return None;
1655        }
1656        match self
1657            .redmine
1658            .json_response_body_page(self.endpoint, self.offset, self.limit)
1659        {
1660            Err(e) => Some(Err(e)),
1661            Ok(ResponsePage {
1662                values,
1663                total_count,
1664                offset,
1665                limit,
1666            }) => {
1667                self.total_count = Some(total_count);
1668                self.offset = offset + limit;
1669                self.reversed_rest = values;
1670                self.reversed_rest.reverse();
1671                if let Some(next) = self.reversed_rest.pop() {
1672                    self.yielded += 1;
1673                    return Some(Ok(next));
1674                }
1675                None
1676            }
1677        }
1678    }
1679
1680    fn size_hint(&self) -> (usize, Option<usize>) {
1681        if let Some(total_count) = self.total_count {
1682            (
1683                self.reversed_rest.len(),
1684                Some((total_count - self.yielded) as usize),
1685            )
1686        } else {
1687            (0, None)
1688        }
1689    }
1690}
1691
1692/// represents an async Stream over all result pages
1693#[pin_project::pin_project]
1694pub struct AllPagesAsync<E, R> {
1695    /// the inner future while we are fetching new data
1696    #[allow(clippy::type_complexity)]
1697    #[pin]
1698    inner: Option<
1699        std::pin::Pin<Box<dyn futures::Future<Output = Result<ResponsePage<R>, crate::Error>>>>,
1700    >,
1701    /// the redmine object to fetch data from
1702    redmine: std::sync::Arc<RedmineAsync>,
1703    /// the endpoint to request data from
1704    endpoint: std::sync::Arc<E>,
1705    /// the offset to fetch next
1706    offset: u64,
1707    /// the limit for each fetch
1708    limit: u64,
1709    /// the cached total count value from the last request
1710    total_count: Option<u64>,
1711    /// the number of elements already yielded
1712    yielded: u64,
1713    /// the cached values from the last fetch that have not been
1714    /// consumed yet, in reverse order to allow pop to remove them
1715    reversed_rest: Vec<R>,
1716}
1717
1718impl<E, R> std::fmt::Debug for AllPagesAsync<E, R>
1719where
1720    R: std::fmt::Debug,
1721{
1722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1723        f.debug_struct("AllPagesAsync")
1724            .field("redmine", &self.redmine)
1725            .field("offset", &self.offset)
1726            .field("limit", &self.limit)
1727            .field("total_count", &self.total_count)
1728            .field("yielded", &self.yielded)
1729            .field("reversed_rest", &self.reversed_rest)
1730            .finish()
1731    }
1732}
1733
1734impl<E, R> AllPagesAsync<E, R> {
1735    /// create a new AllPagesAsync Stream
1736    pub fn new(redmine: std::sync::Arc<RedmineAsync>, endpoint: std::sync::Arc<E>) -> Self {
1737        Self {
1738            inner: None,
1739            redmine,
1740            endpoint,
1741            offset: 0,
1742            limit: 100,
1743            total_count: None,
1744            yielded: 0,
1745            reversed_rest: Vec::new(),
1746        }
1747    }
1748}
1749
1750impl<E, R> futures::stream::Stream for AllPagesAsync<E, R>
1751where
1752    E: Endpoint + ReturnsJsonResponse + Pageable + 'static,
1753    R: DeserializeOwned + std::fmt::Debug + 'static,
1754{
1755    type Item = Result<R, crate::Error>;
1756
1757    fn poll_next(
1758        mut self: std::pin::Pin<&mut Self>,
1759        ctx: &mut std::task::Context<'_>,
1760    ) -> std::task::Poll<Option<Self::Item>> {
1761        if let Some(mut inner) = self.inner.take() {
1762            match inner.as_mut().poll(ctx) {
1763                std::task::Poll::Pending => {
1764                    self.inner = Some(inner);
1765                    std::task::Poll::Pending
1766                }
1767                std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Some(Err(e))),
1768                std::task::Poll::Ready(Ok(ResponsePage {
1769                    values,
1770                    total_count,
1771                    offset,
1772                    limit,
1773                })) => {
1774                    self.total_count = Some(total_count);
1775                    self.offset = offset + limit;
1776                    self.reversed_rest = values;
1777                    self.reversed_rest.reverse();
1778                    if let Some(next) = self.reversed_rest.pop() {
1779                        self.yielded += 1;
1780                        return std::task::Poll::Ready(Some(Ok(next)));
1781                    }
1782                    std::task::Poll::Ready(None)
1783                }
1784            }
1785        } else {
1786            if let Some(next) = self.reversed_rest.pop() {
1787                self.yielded += 1;
1788                return std::task::Poll::Ready(Some(Ok(next)));
1789            }
1790            if let Some(total_count) = self.total_count
1791                && self.offset > total_count
1792            {
1793                return std::task::Poll::Ready(None);
1794            }
1795            self.inner = Some(
1796                self.redmine
1797                    .clone()
1798                    .json_response_body_page(self.endpoint.clone(), self.offset, self.limit)
1799                    .boxed_local(),
1800            );
1801            self.poll_next(ctx)
1802        }
1803    }
1804
1805    fn size_hint(&self) -> (usize, Option<usize>) {
1806        if let Some(total_count) = self.total_count {
1807            (
1808                self.reversed_rest.len(),
1809                Some((total_count - self.yielded) as usize),
1810            )
1811        } else {
1812            (0, None)
1813        }
1814    }
1815}
1816
1817/// trait to allow both `&E` and `std::sync::Arc<E>` as parameters for endpoints
1818/// we can not just use Into because that tries to treat &Endpoint as the value E
1819/// and screws up our other trait bounds
1820///
1821/// if we just used Arc the users would have to change all old call sites
1822pub trait EndpointParameter<E> {
1823    /// convert the endpoint parameter into an Arc
1824    fn into_arc(self) -> std::sync::Arc<E>;
1825}
1826
1827impl<E> EndpointParameter<E> for &E
1828where
1829    E: Clone,
1830{
1831    fn into_arc(self) -> std::sync::Arc<E> {
1832        std::sync::Arc::new(self.to_owned())
1833    }
1834}
1835
1836impl<E> EndpointParameter<E> for std::sync::Arc<E> {
1837    fn into_arc(self) -> std::sync::Arc<E> {
1838        self
1839    }
1840}