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