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]
175 #[allow(clippy::missing_panics_doc)]
176 pub fn issue_url(&self, issue_id: u64) -> Url {
177 let Redmine { redmine_url, .. } = self;
178 redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
181 }
182
183 fn rest(
186 &self,
187 method: reqwest::Method,
188 endpoint: &str,
189 parameters: QueryParams,
190 mime_type_and_body: Option<(&str, Vec<u8>)>,
191 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
192 let Redmine {
193 client,
194 redmine_url,
195 api_key,
196 impersonate_user_id,
197 } = self;
198 let mut url = redmine_url.join(endpoint)?;
199 parameters.add_to_url(&mut url);
200 debug!(%url, %method, "Calling redmine");
201 let req = client
202 .request(method.clone(), url.clone())
203 .header("x-redmine-api-key", api_key);
204 let req = if let Some(user_id) = impersonate_user_id {
205 req.header("X-Redmine-Switch-User", format!("{user_id}"))
206 } else {
207 req
208 };
209 let req = if let Some((mime, data)) = mime_type_and_body {
210 if let Ok(request_body) = from_utf8(&data) {
211 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
212 } else {
213 trace!(
214 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
215 mime, data
216 );
217 }
218 req.body(data).header("Content-Type", mime)
219 } else {
220 req
221 };
222 let result = req.send();
223 if let Err(ref e) = result {
224 error!(%url, %method, "Redmine send error: {:?}", e);
225 }
226 let result = result?;
227 let status = result.status();
228 let response_body = result.bytes()?;
229 match from_utf8(&response_body) {
230 Ok(response_body) => {
231 trace!("Response body:\n{}", &response_body);
232 }
233 Err(e) => {
234 trace!(
235 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
236 &e, &response_body
237 );
238 }
239 }
240 if status.is_client_error() {
241 error!(%url, %method, "Redmine status error (client error): {:?}", status);
242 return Err(crate::Error::HttpErrorResponse(status));
243 } else if status.is_server_error() {
244 error!(%url, %method, "Redmine status error (server error): {:?}", status);
245 return Err(crate::Error::HttpErrorResponse(status));
246 }
247 Ok((status, response_body))
248 }
249
250 pub fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
258 where
259 E: Endpoint,
260 {
261 let method = endpoint.method();
262 let url = endpoint.endpoint();
263 let parameters = endpoint.parameters();
264 let mime_type_and_body = endpoint.body()?;
265 self.rest(method, &url, parameters, mime_type_and_body)?;
266 Ok(())
267 }
268
269 pub fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
277 where
278 E: Endpoint + ReturnsJsonResponse + NoPagination,
279 R: DeserializeOwned + std::fmt::Debug,
280 {
281 let method = endpoint.method();
282 let url = endpoint.endpoint();
283 let parameters = endpoint.parameters();
284 let mime_type_and_body = endpoint.body()?;
285 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
286 if response_body.is_empty() {
287 Err(crate::Error::EmptyResponseBody(status))
288 } else {
289 let result = serde_json::from_slice::<R>(&response_body);
290 if let Ok(ref parsed_response_body) = result {
291 trace!("Parsed response body:\n{:#?}", parsed_response_body);
292 }
293 Ok(result?)
294 }
295 }
296
297 pub fn json_response_body_page<E, R>(
305 &self,
306 endpoint: &E,
307 offset: u64,
308 limit: u64,
309 ) -> Result<ResponsePage<R>, crate::Error>
310 where
311 E: Endpoint + ReturnsJsonResponse + Pageable,
312 R: DeserializeOwned + std::fmt::Debug,
313 {
314 let method = endpoint.method();
315 let url = endpoint.endpoint();
316 let mut parameters = endpoint.parameters();
317 parameters.push("offset", offset);
318 parameters.push("limit", limit);
319 let mime_type_and_body = endpoint.body()?;
320 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
321 if response_body.is_empty() {
322 Err(crate::Error::EmptyResponseBody(status))
323 } else {
324 let json_value_response_body: serde_json::Value =
325 serde_json::from_slice(&response_body)?;
326 let json_object_response_body = json_value_response_body.as_object();
327 if let Some(json_object_response_body) = json_object_response_body {
328 let total_count = json_object_response_body
329 .get("total_count")
330 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
331 .as_u64()
332 .ok_or_else(|| {
333 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
334 })?;
335 let offset = json_object_response_body
336 .get("offset")
337 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
338 .as_u64()
339 .ok_or_else(|| {
340 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
341 })?;
342 let limit = json_object_response_body
343 .get("limit")
344 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
345 .as_u64()
346 .ok_or_else(|| {
347 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
348 })?;
349 let response_wrapper_key = endpoint.response_wrapper_key();
350 let inner_response_body = json_object_response_body
351 .get(&response_wrapper_key)
352 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
353 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
354 if let Ok(ref parsed_response_body) = result {
355 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
356 }
357 Ok(ResponsePage {
358 values: result?,
359 total_count,
360 offset,
361 limit,
362 })
363 } else {
364 Err(crate::Error::NonObjectResponseBody(status))
365 }
366 }
367 }
368
369 pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
379 where
380 E: Endpoint + ReturnsJsonResponse + Pageable,
381 R: DeserializeOwned + std::fmt::Debug,
382 {
383 let method = endpoint.method();
384 let url = endpoint.endpoint();
385 let mut offset = 0;
386 let limit = 100;
387 let mut total_results = vec![];
388 loop {
389 let mut page_parameters = endpoint.parameters();
390 page_parameters.push("offset", offset);
391 page_parameters.push("limit", limit);
392 let mime_type_and_body = endpoint.body()?;
393 let (status, response_body) =
394 self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
395 if response_body.is_empty() {
396 return Err(crate::Error::EmptyResponseBody(status));
397 }
398 let json_value_response_body: serde_json::Value =
399 serde_json::from_slice(&response_body)?;
400 let json_object_response_body = json_value_response_body.as_object();
401 if let Some(json_object_response_body) = json_object_response_body {
402 let total_count: u64 = json_object_response_body
403 .get("total_count")
404 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
405 .as_u64()
406 .ok_or_else(|| {
407 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
408 })?;
409 let response_offset: u64 = json_object_response_body
410 .get("offset")
411 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
412 .as_u64()
413 .ok_or_else(|| {
414 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
415 })?;
416 let response_limit: u64 = json_object_response_body
417 .get("limit")
418 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
419 .as_u64()
420 .ok_or_else(|| {
421 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
422 })?;
423 let response_wrapper_key = endpoint.response_wrapper_key();
424 let inner_response_body = json_object_response_body
425 .get(&response_wrapper_key)
426 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
427 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
428 if let Ok(ref parsed_response_body) = result {
429 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
430 }
431 total_results.extend(result?);
432 if total_count < (response_offset + response_limit) {
433 break;
434 }
435 offset += limit;
436 } else {
437 return Err(crate::Error::NonObjectResponseBody(status));
438 }
439 }
440 Ok(total_results)
441 }
442
443 pub fn json_response_body_all_pages_iter<'a, 'e, 'i, E, R>(
446 &'a self,
447 endpoint: &'e E,
448 ) -> AllPages<'i, E, R>
449 where
450 E: Endpoint + ReturnsJsonResponse + Pageable,
451 R: DeserializeOwned + std::fmt::Debug,
452 'a: 'i,
453 'e: 'i,
454 {
455 AllPages::new(self, endpoint)
456 }
457}
458
459impl RedmineAsync {
460 pub fn new(
466 client: reqwest::Client,
467 redmine_url: url::Url,
468 api_key: &str,
469 ) -> Result<std::sync::Arc<Self>, crate::Error> {
470 Ok(std::sync::Arc::new(Self {
471 client,
472 redmine_url,
473 api_key: api_key.to_string(),
474 impersonate_user_id: None,
475 }))
476 }
477
478 pub fn from_env(client: reqwest::Client) -> Result<std::sync::Arc<Self>, crate::Error> {
488 let env_options = envy::from_env::<EnvOptions>()?;
489
490 let redmine_url = env_options.redmine_url;
491 let api_key = env_options.redmine_api_key;
492
493 Self::new(client, redmine_url, &api_key)
494 }
495
496 pub fn impersonate_user(&mut self, id: u64) {
500 self.impersonate_user_id = Some(id);
501 }
502
503 #[must_use]
508 #[allow(clippy::missing_panics_doc)]
509 pub fn issue_url(&self, issue_id: u64) -> Url {
510 let RedmineAsync { redmine_url, .. } = self;
511 redmine_url.join(&format!("/issues/{issue_id}")).unwrap()
514 }
515
516 async fn rest(
519 self: std::sync::Arc<Self>,
520 method: reqwest::Method,
521 endpoint: &str,
522 parameters: QueryParams<'_>,
523 mime_type_and_body: Option<(&str, Vec<u8>)>,
524 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
525 let RedmineAsync {
526 client,
527 redmine_url,
528 api_key,
529 impersonate_user_id,
530 } = self.as_ref();
531 let mut url = redmine_url.join(endpoint)?;
532 parameters.add_to_url(&mut url);
533 debug!(%url, %method, "Calling redmine");
534 let req = client
535 .request(method.clone(), url.clone())
536 .header("x-redmine-api-key", api_key);
537 let req = if let Some(user_id) = impersonate_user_id {
538 req.header("X-Redmine-Switch-User", format!("{user_id}"))
539 } else {
540 req
541 };
542 let req = if let Some((mime, data)) = mime_type_and_body {
543 if let Ok(request_body) = from_utf8(&data) {
544 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
545 } else {
546 trace!(
547 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
548 mime, data
549 );
550 }
551 req.body(data).header("Content-Type", mime)
552 } else {
553 req
554 };
555 let result = req.send().await;
556 if let Err(ref e) = result {
557 error!(%url, %method, "Redmine send error: {:?}", e);
558 }
559 let result = result?;
560 let status = result.status();
561 let response_body = result.bytes().await?;
562 match from_utf8(&response_body) {
563 Ok(response_body) => {
564 trace!("Response body:\n{}", &response_body);
565 }
566 Err(e) => {
567 trace!(
568 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
569 &e, &response_body
570 );
571 }
572 }
573 if status.is_client_error() {
574 error!(%url, %method, "Redmine status error (client error): {:?}", status);
575 } else if status.is_server_error() {
576 error!(%url, %method, "Redmine status error (server error): {:?}", status);
577 }
578 Ok((status, response_body))
579 }
580
581 pub async fn ignore_response_body<E>(
589 self: std::sync::Arc<Self>,
590 endpoint: impl EndpointParameter<E>,
591 ) -> Result<(), crate::Error>
592 where
593 E: Endpoint,
594 {
595 let endpoint: std::sync::Arc<E> = endpoint.into_arc();
596 let method = endpoint.method();
597 let url = endpoint.endpoint();
598 let parameters = endpoint.parameters();
599 let mime_type_and_body = endpoint.body()?;
600 self.rest(method, &url, parameters, mime_type_and_body)
601 .await?;
602 Ok(())
603 }
604
605 pub async fn json_response_body<E, R>(
615 self: std::sync::Arc<Self>,
616 endpoint: impl EndpointParameter<E>,
617 ) -> Result<R, crate::Error>
618 where
619 E: Endpoint + ReturnsJsonResponse + NoPagination,
620 R: DeserializeOwned + std::fmt::Debug,
621 {
622 let endpoint: std::sync::Arc<E> = endpoint.into_arc();
623 let method = endpoint.method();
624 let url = endpoint.endpoint();
625 let parameters = endpoint.parameters();
626 let mime_type_and_body = endpoint.body()?;
627 let (status, response_body) = self
628 .rest(method, &url, parameters, mime_type_and_body)
629 .await?;
630 if response_body.is_empty() {
631 Err(crate::Error::EmptyResponseBody(status))
632 } else {
633 let result = serde_json::from_slice::<R>(&response_body);
634 if let Ok(ref parsed_response_body) = result {
635 trace!("Parsed response body:\n{:#?}", parsed_response_body);
636 }
637 Ok(result?)
638 }
639 }
640
641 pub async fn json_response_body_page<E, R>(
649 self: std::sync::Arc<Self>,
650 endpoint: impl EndpointParameter<E>,
651 offset: u64,
652 limit: u64,
653 ) -> Result<ResponsePage<R>, crate::Error>
654 where
655 E: Endpoint + ReturnsJsonResponse + Pageable,
656 R: DeserializeOwned + std::fmt::Debug,
657 {
658 let endpoint: std::sync::Arc<E> = endpoint.into_arc();
659 let method = endpoint.method();
660 let url = endpoint.endpoint();
661 let mut parameters = endpoint.parameters();
662 parameters.push("offset", offset);
663 parameters.push("limit", limit);
664 let mime_type_and_body = endpoint.body()?;
665 let (status, response_body) = self
666 .rest(method, &url, parameters, mime_type_and_body)
667 .await?;
668 if response_body.is_empty() {
669 Err(crate::Error::EmptyResponseBody(status))
670 } else {
671 let json_value_response_body: serde_json::Value =
672 serde_json::from_slice(&response_body)?;
673 let json_object_response_body = json_value_response_body.as_object();
674 if let Some(json_object_response_body) = json_object_response_body {
675 let total_count = json_object_response_body
676 .get("total_count")
677 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
678 .as_u64()
679 .ok_or_else(|| {
680 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
681 })?;
682 let offset = json_object_response_body
683 .get("offset")
684 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
685 .as_u64()
686 .ok_or_else(|| {
687 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
688 })?;
689 let limit = json_object_response_body
690 .get("limit")
691 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
692 .as_u64()
693 .ok_or_else(|| {
694 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
695 })?;
696 let response_wrapper_key = endpoint.response_wrapper_key();
697 let inner_response_body = json_object_response_body
698 .get(&response_wrapper_key)
699 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
700 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
701 if let Ok(ref parsed_response_body) = result {
702 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
703 }
704 Ok(ResponsePage {
705 values: result?,
706 total_count,
707 offset,
708 limit,
709 })
710 } else {
711 Err(crate::Error::NonObjectResponseBody(status))
712 }
713 }
714 }
715
716 pub async fn json_response_body_all_pages<E, R>(
726 self: std::sync::Arc<Self>,
727 endpoint: impl EndpointParameter<E>,
728 ) -> Result<Vec<R>, crate::Error>
729 where
730 E: Endpoint + ReturnsJsonResponse + Pageable,
731 R: DeserializeOwned + std::fmt::Debug,
732 {
733 let endpoint: std::sync::Arc<E> = endpoint.into_arc();
734 let method = endpoint.method();
735 let url = endpoint.endpoint();
736 let mut offset = 0;
737 let limit = 100;
738 let mut total_results = vec![];
739 loop {
740 let mut page_parameters = endpoint.parameters();
741 page_parameters.push("offset", offset);
742 page_parameters.push("limit", limit);
743 let mime_type_and_body = endpoint.body()?;
744 let (status, response_body) = self
745 .clone()
746 .rest(method.clone(), &url, page_parameters, mime_type_and_body)
747 .await?;
748 if response_body.is_empty() {
749 return Err(crate::Error::EmptyResponseBody(status));
750 }
751 let json_value_response_body: serde_json::Value =
752 serde_json::from_slice(&response_body)?;
753 let json_object_response_body = json_value_response_body.as_object();
754 if let Some(json_object_response_body) = json_object_response_body {
755 let total_count: u64 = json_object_response_body
756 .get("total_count")
757 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
758 .as_u64()
759 .ok_or_else(|| {
760 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
761 })?;
762 let response_offset: u64 = json_object_response_body
763 .get("offset")
764 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
765 .as_u64()
766 .ok_or_else(|| {
767 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
768 })?;
769 let response_limit: u64 = json_object_response_body
770 .get("limit")
771 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
772 .as_u64()
773 .ok_or_else(|| {
774 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
775 })?;
776 let response_wrapper_key = endpoint.response_wrapper_key();
777 let inner_response_body = json_object_response_body
778 .get(&response_wrapper_key)
779 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
780 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
781 if let Ok(ref parsed_response_body) = result {
782 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
783 }
784 total_results.extend(result?);
785 if total_count < (response_offset + response_limit) {
786 break;
787 }
788 offset += limit;
789 } else {
790 return Err(crate::Error::NonObjectResponseBody(status));
791 }
792 }
793 Ok(total_results)
794 }
795
796 pub fn json_response_body_all_pages_stream<E, R>(
799 self: std::sync::Arc<Self>,
800 endpoint: impl EndpointParameter<E>,
801 ) -> AllPagesAsync<E, R>
802 where
803 E: Endpoint + ReturnsJsonResponse + Pageable,
804 R: DeserializeOwned + std::fmt::Debug,
805 {
806 let endpoint: std::sync::Arc<E> = endpoint.into_arc();
807 AllPagesAsync::new(self, endpoint)
808 }
809}
810
811pub trait ParamValue<'a> {
813 #[allow(clippy::wrong_self_convention)]
814 fn as_value(&self) -> Cow<'a, str>;
816}
817
818impl ParamValue<'static> for bool {
819 fn as_value(&self) -> Cow<'static, str> {
820 if *self { "true".into() } else { "false".into() }
821 }
822}
823
824impl<'a> ParamValue<'a> for &'a str {
825 fn as_value(&self) -> Cow<'a, str> {
826 (*self).into()
827 }
828}
829
830impl ParamValue<'static> for String {
831 fn as_value(&self) -> Cow<'static, str> {
832 self.clone().into()
833 }
834}
835
836impl<'a> ParamValue<'a> for &'a String {
837 fn as_value(&self) -> Cow<'a, str> {
838 (*self).into()
839 }
840}
841
842impl<T> ParamValue<'static> for Vec<T>
845where
846 T: ToString,
847{
848 fn as_value(&self) -> Cow<'static, str> {
849 self.iter()
850 .map(|e| e.to_string())
851 .collect::<Vec<_>>()
852 .join(",")
853 .into()
854 }
855}
856
857impl<'a, T> ParamValue<'a> for &'a Vec<T>
860where
861 T: ToString,
862{
863 fn as_value(&self) -> Cow<'a, str> {
864 self.iter()
865 .map(|e| e.to_string())
866 .collect::<Vec<_>>()
867 .join(",")
868 .into()
869 }
870}
871
872impl<'a> ParamValue<'a> for Cow<'a, str> {
873 fn as_value(&self) -> Cow<'a, str> {
874 self.clone()
875 }
876}
877
878impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
879 fn as_value(&self) -> Cow<'a, str> {
880 (*self).clone()
881 }
882}
883
884impl ParamValue<'static> for u64 {
885 fn as_value(&self) -> Cow<'static, str> {
886 format!("{self}").into()
887 }
888}
889
890impl ParamValue<'static> for f64 {
891 fn as_value(&self) -> Cow<'static, str> {
892 format!("{self}").into()
893 }
894}
895
896impl ParamValue<'static> for time::OffsetDateTime {
897 fn as_value(&self) -> Cow<'static, str> {
898 self.format(&time::format_description::well_known::Rfc3339)
899 .unwrap()
900 .into()
901 }
902}
903
904impl ParamValue<'static> for time::Date {
905 fn as_value(&self) -> Cow<'static, str> {
906 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
907 self.format(&format).unwrap().into()
908 }
909}
910
911#[derive(Debug, Default, Clone)]
913pub struct QueryParams<'a> {
914 params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
916}
917
918impl<'a> QueryParams<'a> {
919 pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
921 where
922 K: Into<Cow<'a, str>>,
923 V: ParamValue<'b>,
924 'b: 'a,
925 {
926 self.params.push((key.into(), value.as_value()));
927 self
928 }
929
930 pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
932 where
933 K: Into<Cow<'a, str>>,
934 V: ParamValue<'b>,
935 'b: 'a,
936 {
937 if let Some(value) = value {
938 self.params.push((key.into(), value.as_value()));
939 }
940 self
941 }
942
943 pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
945 where
946 I: Iterator<Item = (K, V)>,
947 K: Into<Cow<'a, str>>,
948 V: ParamValue<'b>,
949 'b: 'a,
950 {
951 self.params
952 .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
953 self
954 }
955
956 pub fn add_to_url(&self, url: &mut Url) {
958 let mut pairs = url.query_pairs_mut();
959 pairs.extend_pairs(self.params.iter());
960 }
961}
962
963pub trait Endpoint {
965 fn method(&self) -> Method;
967 fn endpoint(&self) -> Cow<'static, str>;
969
970 fn parameters(&self) -> QueryParams<'_> {
972 QueryParams::default()
973 }
974
975 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
983 Ok(None)
984 }
985}
986
987pub trait ReturnsJsonResponse {}
989
990#[diagnostic::on_unimplemented(
994 message = "{Self} is an endpoint that either returns nothing or requires pagination, use `.ignore_response_body(&endpoint)`, `.json_response_body_page(&endpoint, offset, limit)` or `.json_response_body_all_pages(&endpoint)` instead of `.json_response_body(&endpoint)`"
995)]
996pub trait NoPagination {}
997
998#[diagnostic::on_unimplemented(
1000 message = "{Self} is an endpoint that does not implement pagination or returns nothing, use `.ignore_response_body(&endpoint)` or `.json_response_body(&endpoint)` instead of `.json_response_body_page(&endpoint, offset, limit)` or `.json_response_body_all_pages(&endpoint)`"
1001)]
1002pub trait Pageable {
1003 fn response_wrapper_key(&self) -> String;
1005}
1006
1007pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
1015where
1016 D: serde::Deserializer<'de>,
1017{
1018 let s = String::deserialize(deserializer)?;
1019
1020 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1021 .map_err(serde::de::Error::custom)
1022}
1023
1024pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1032where
1033 S: serde::Serializer,
1034{
1035 let s = t
1036 .format(&time::format_description::well_known::Rfc3339)
1037 .map_err(serde::ser::Error::custom)?;
1038
1039 s.serialize(serializer)
1040}
1041
1042pub fn deserialize_optional_rfc3339<'de, D>(
1050 deserializer: D,
1051) -> Result<Option<time::OffsetDateTime>, D::Error>
1052where
1053 D: serde::Deserializer<'de>,
1054{
1055 let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1056
1057 if let Some(s) = s {
1058 Ok(Some(
1059 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1060 .map_err(serde::de::Error::custom)?,
1061 ))
1062 } else {
1063 Ok(None)
1064 }
1065}
1066
1067pub fn serialize_optional_rfc3339<S>(
1075 t: &Option<time::OffsetDateTime>,
1076 serializer: S,
1077) -> Result<S::Ok, S::Error>
1078where
1079 S: serde::Serializer,
1080{
1081 if let Some(t) = t {
1082 let s = t
1083 .format(&time::format_description::well_known::Rfc3339)
1084 .map_err(serde::ser::Error::custom)?;
1085
1086 s.serialize(serializer)
1087 } else {
1088 let n: Option<String> = None;
1089 n.serialize(serializer)
1090 }
1091}
1092
1093#[derive(Debug)]
1095pub struct AllPages<'i, E, R> {
1096 redmine: &'i Redmine,
1098 endpoint: &'i E,
1100 offset: u64,
1102 limit: u64,
1104 total_count: Option<u64>,
1106 yielded: u64,
1108 reversed_rest: Vec<R>,
1111}
1112
1113impl<'i, E, R> AllPages<'i, E, R> {
1114 pub fn new(redmine: &'i Redmine, endpoint: &'i E) -> Self {
1116 Self {
1117 redmine,
1118 endpoint,
1119 offset: 0,
1120 limit: 100,
1121 total_count: None,
1122 yielded: 0,
1123 reversed_rest: Vec::new(),
1124 }
1125 }
1126}
1127
1128impl<'i, E, R> Iterator for AllPages<'i, E, R>
1129where
1130 E: Endpoint + ReturnsJsonResponse + Pageable,
1131 R: DeserializeOwned + std::fmt::Debug,
1132{
1133 type Item = Result<R, crate::Error>;
1134
1135 fn next(&mut self) -> Option<Self::Item> {
1136 if let Some(next) = self.reversed_rest.pop() {
1137 self.yielded += 1;
1138 return Some(Ok(next));
1139 }
1140 if let Some(total_count) = self.total_count
1141 && self.offset > total_count
1142 {
1143 return None;
1144 }
1145 match self
1146 .redmine
1147 .json_response_body_page(self.endpoint, self.offset, self.limit)
1148 {
1149 Err(e) => Some(Err(e)),
1150 Ok(ResponsePage {
1151 values,
1152 total_count,
1153 offset,
1154 limit,
1155 }) => {
1156 self.total_count = Some(total_count);
1157 self.offset = offset + limit;
1158 self.reversed_rest = values;
1159 self.reversed_rest.reverse();
1160 if let Some(next) = self.reversed_rest.pop() {
1161 self.yielded += 1;
1162 return Some(Ok(next));
1163 }
1164 None
1165 }
1166 }
1167 }
1168
1169 fn size_hint(&self) -> (usize, Option<usize>) {
1170 if let Some(total_count) = self.total_count {
1171 (
1172 self.reversed_rest.len(),
1173 Some((total_count - self.yielded) as usize),
1174 )
1175 } else {
1176 (0, None)
1177 }
1178 }
1179}
1180
1181#[pin_project::pin_project]
1183pub struct AllPagesAsync<E, R> {
1184 #[allow(clippy::type_complexity)]
1186 #[pin]
1187 inner: Option<
1188 std::pin::Pin<Box<dyn futures::Future<Output = Result<ResponsePage<R>, crate::Error>>>>,
1189 >,
1190 redmine: std::sync::Arc<RedmineAsync>,
1192 endpoint: std::sync::Arc<E>,
1194 offset: u64,
1196 limit: u64,
1198 total_count: Option<u64>,
1200 yielded: u64,
1202 reversed_rest: Vec<R>,
1205}
1206
1207impl<E, R> std::fmt::Debug for AllPagesAsync<E, R>
1208where
1209 R: std::fmt::Debug,
1210{
1211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212 f.debug_struct("AllPagesAsync")
1213 .field("redmine", &self.redmine)
1214 .field("offset", &self.offset)
1215 .field("limit", &self.limit)
1216 .field("total_count", &self.total_count)
1217 .field("yielded", &self.yielded)
1218 .field("reversed_rest", &self.reversed_rest)
1219 .finish()
1220 }
1221}
1222
1223impl<E, R> AllPagesAsync<E, R> {
1224 pub fn new(redmine: std::sync::Arc<RedmineAsync>, endpoint: std::sync::Arc<E>) -> Self {
1226 Self {
1227 inner: None,
1228 redmine,
1229 endpoint,
1230 offset: 0,
1231 limit: 100,
1232 total_count: None,
1233 yielded: 0,
1234 reversed_rest: Vec::new(),
1235 }
1236 }
1237}
1238
1239impl<E, R> futures::stream::Stream for AllPagesAsync<E, R>
1240where
1241 E: Endpoint + ReturnsJsonResponse + Pageable + 'static,
1242 R: DeserializeOwned + std::fmt::Debug + 'static,
1243{
1244 type Item = Result<R, crate::Error>;
1245
1246 fn poll_next(
1247 mut self: std::pin::Pin<&mut Self>,
1248 ctx: &mut std::task::Context<'_>,
1249 ) -> std::task::Poll<Option<Self::Item>> {
1250 if let Some(mut inner) = self.inner.take() {
1251 match inner.as_mut().poll(ctx) {
1252 std::task::Poll::Pending => {
1253 self.inner = Some(inner);
1254 std::task::Poll::Pending
1255 }
1256 std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Some(Err(e))),
1257 std::task::Poll::Ready(Ok(ResponsePage {
1258 values,
1259 total_count,
1260 offset,
1261 limit,
1262 })) => {
1263 self.total_count = Some(total_count);
1264 self.offset = offset + limit;
1265 self.reversed_rest = values;
1266 self.reversed_rest.reverse();
1267 if let Some(next) = self.reversed_rest.pop() {
1268 self.yielded += 1;
1269 return std::task::Poll::Ready(Some(Ok(next)));
1270 }
1271 std::task::Poll::Ready(None)
1272 }
1273 }
1274 } else {
1275 if let Some(next) = self.reversed_rest.pop() {
1276 self.yielded += 1;
1277 return std::task::Poll::Ready(Some(Ok(next)));
1278 }
1279 if let Some(total_count) = self.total_count
1280 && self.offset > total_count
1281 {
1282 return std::task::Poll::Ready(None);
1283 }
1284 self.inner = Some(
1285 self.redmine
1286 .clone()
1287 .json_response_body_page(self.endpoint.clone(), self.offset, self.limit)
1288 .boxed_local(),
1289 );
1290 self.poll_next(ctx)
1291 }
1292 }
1293
1294 fn size_hint(&self) -> (usize, Option<usize>) {
1295 if let Some(total_count) = self.total_count {
1296 (
1297 self.reversed_rest.len(),
1298 Some((total_count - self.yielded) as usize),
1299 )
1300 } else {
1301 (0, None)
1302 }
1303 }
1304}
1305
1306pub trait EndpointParameter<E> {
1312 fn into_arc(self) -> std::sync::Arc<E>;
1314}
1315
1316impl<E> EndpointParameter<E> for &E
1317where
1318 E: Clone,
1319{
1320 fn into_arc(self) -> std::sync::Arc<E> {
1321 std::sync::Arc::new(self.to_owned())
1322 }
1323}
1324
1325impl<E> EndpointParameter<E> for std::sync::Arc<E> {
1326 fn into_arc(self) -> std::sync::Arc<E> {
1327 self
1328 }
1329}