1pub 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#[derive(derive_more::Debug)]
65pub struct Redmine {
66 client: reqwest::blocking::Client,
68 redmine_url: Url,
70 #[debug(skip)]
72 api_key: String,
73 impersonate_user_id: Option<u64>,
75}
76
77#[derive(derive_more::Debug)]
79pub struct RedmineAsync {
80 client: reqwest::Client,
82 redmine_url: Url,
84 #[debug(skip)]
86 api_key: String,
87 impersonate_user_id: Option<u64>,
89}
90
91fn 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#[derive(Debug, Clone, serde::Deserialize)]
103struct EnvOptions {
104 redmine_api_key: String,
106
107 #[serde(deserialize_with = "parse_url")]
109 redmine_url: url::Url,
110}
111
112#[derive(Debug, Clone)]
115pub struct ResponsePage<T> {
116 pub values: Vec<T>,
118 pub total_count: u64,
120 pub offset: u64,
122 pub limit: u64,
124}
125
126impl Redmine {
127 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 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 pub fn impersonate_user(&mut self, id: u64) {
167 self.impersonate_user_id = Some(id);
168 }
169
170 #[must_use]
172 pub fn redmine_url(&self) -> &Url {
173 &self.redmine_url
174 }
175
176 #[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 redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
187 }
188
189 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 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 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 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 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 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 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 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 pub fn impersonate_user(&mut self, id: u64) {
506 self.impersonate_user_id = Some(id);
507 }
508
509 #[must_use]
511 pub fn redmine_url(&self) -> &Url {
512 &self.redmine_url
513 }
514
515 #[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 redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
526 }
527
528 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 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 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 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 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 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
823pub trait ParamValue<'a> {
825 #[allow(clippy::wrong_self_convention)]
826 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
854impl<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
869impl<'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#[derive(Debug, Clone)]
926pub enum DateTimeFilterPast {
927 ExactMatch(time::OffsetDateTime),
929 Range(time::OffsetDateTime, time::OffsetDateTime),
931 LessThanOrEqual(time::OffsetDateTime),
933 GreaterThanOrEqual(time::OffsetDateTime),
935 LessThanDaysAgo(u32),
937 MoreThanDaysAgo(u32),
939 WithinPastDays(u32),
941 ExactDaysAgo(u32),
943 Today,
945 Yesterday,
947 ThisWeek,
949 LastWeek,
951 LastTwoWeeks,
953 ThisMonth,
955 LastMonth,
957 ThisYear,
959 Unset,
961 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#[derive(Debug, Clone)]
1057pub enum StringFieldFilter {
1058 ExactMatch(String),
1060 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#[derive(Debug, Clone)]
1079pub struct CustomFieldFilter {
1080 pub id: u64,
1082 pub value: StringFieldFilter,
1084}
1085
1086#[derive(Debug, Clone)]
1088pub enum FloatFilter {
1089 ExactMatch(f64),
1091 Range(f64, f64),
1093 LessThanOrEqual(f64),
1095 GreaterThanOrEqual(f64),
1097 Any,
1099 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#[derive(Debug, Clone)]
1118pub enum IntegerFilter {
1119 ExactMatch(u64),
1121 Range(u64, u64),
1123 LessThanOrEqual(u64),
1125 GreaterThanOrEqual(u64),
1127 Any,
1129 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#[derive(Debug, Clone)]
1148pub enum TrackerFilter {
1149 Any,
1151 None,
1153 TheseTrackers(Vec<u64>),
1155 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#[derive(Debug, Clone)]
1186pub enum ActivityFilter {
1187 Any,
1189 None,
1191 TheseActivities(Vec<u64>),
1193 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#[derive(Debug, Clone)]
1224pub enum VersionFilter {
1225 Any,
1227 None,
1229 TheseVersions(Vec<u64>),
1231 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#[derive(Debug, Clone)]
1262pub enum DateFilter {
1263 ExactMatch(time::Date),
1265 Range(time::Date, time::Date),
1267 LessThanOrEqual(time::Date),
1269 GreaterThanOrEqual(time::Date),
1271 LessThanDaysAgo(u32),
1273 MoreThanDaysAgo(u32),
1275 WithinPastDays(u32),
1277 ExactDaysAgo(u32),
1279 InLessThanDays(u32),
1281 InMoreThanDays(u32),
1283 WithinFutureDays(u32),
1285 InExactDays(u32),
1287 Today,
1289 Yesterday,
1291 Tomorrow,
1293 ThisWeek,
1295 LastWeek,
1297 LastTwoWeeks,
1299 NextWeek,
1301 ThisMonth,
1303 LastMonth,
1305 NextMonth,
1307 ThisYear,
1309 Unset,
1311 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#[derive(Debug, Default, Clone)]
1424pub struct QueryParams<'a> {
1425 params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
1427}
1428
1429impl<'a> QueryParams<'a> {
1430 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 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 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 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
1474pub trait Endpoint {
1476 fn method(&self) -> Method;
1478 fn endpoint(&self) -> Cow<'static, str>;
1480
1481 fn parameters(&self) -> QueryParams<'_> {
1483 QueryParams::default()
1484 }
1485
1486 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1494 Ok(None)
1495 }
1496}
1497
1498pub trait ReturnsJsonResponse {}
1500
1501#[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#[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 fn response_wrapper_key(&self) -> String;
1516}
1517
1518pub 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
1535pub 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
1553pub 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
1578pub 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#[derive(Debug)]
1606pub struct AllPages<'i, E, R> {
1607 redmine: &'i Redmine,
1609 endpoint: &'i E,
1611 offset: u64,
1613 limit: u64,
1615 total_count: Option<u64>,
1617 yielded: u64,
1619 reversed_rest: Vec<R>,
1622}
1623
1624impl<'i, E, R> AllPages<'i, E, R> {
1625 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#[pin_project::pin_project]
1694pub struct AllPagesAsync<E, R> {
1695 #[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 redmine: std::sync::Arc<RedmineAsync>,
1703 endpoint: std::sync::Arc<E>,
1705 offset: u64,
1707 limit: u64,
1709 total_count: Option<u64>,
1711 yielded: u64,
1713 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 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
1817pub trait EndpointParameter<E> {
1823 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}