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 std::str::from_utf8;
49
50use serde::de::DeserializeOwned;
51use serde::Deserialize;
52use serde::Deserializer;
53use serde::Serialize;
54
55use reqwest::Method;
56use std::borrow::Cow;
57
58use reqwest::Url;
59use tracing::{debug, error, trace};
60
61#[derive(derive_more::Debug)]
63pub struct Redmine {
64 client: reqwest::blocking::Client,
66 redmine_url: Url,
68 #[debug(skip)]
70 api_key: String,
71 impersonate_user_id: Option<u64>,
73}
74
75#[derive(derive_more::Debug)]
77pub struct RedmineAsync {
78 client: reqwest::Client,
80 redmine_url: Url,
82 #[debug(skip)]
84 api_key: String,
85 impersonate_user_id: Option<u64>,
87}
88
89fn parse_url<'de, D>(deserializer: D) -> Result<url::Url, D::Error>
91where
92 D: Deserializer<'de>,
93{
94 let buf = String::deserialize(deserializer)?;
95
96 url::Url::parse(&buf).map_err(serde::de::Error::custom)
97}
98
99#[derive(Debug, Clone, serde::Deserialize)]
101struct EnvOptions {
102 redmine_api_key: String,
104
105 #[serde(deserialize_with = "parse_url")]
107 redmine_url: url::Url,
108}
109
110#[derive(Debug, Clone)]
113pub struct ResponsePage<T> {
114 pub values: Vec<T>,
116 pub total_count: u64,
118 pub offset: u64,
120 pub limit: u64,
122}
123
124impl Redmine {
125 pub fn new(redmine_url: url::Url, api_key: &str) -> Result<Self, crate::Error> {
131 #[cfg(not(feature = "rustls-tls"))]
132 let client = reqwest::blocking::Client::new();
133 #[cfg(feature = "rustls-tls")]
134 let client = reqwest::blocking::Client::builder()
135 .use_rustls_tls()
136 .build()?;
137
138 Ok(Self {
139 client,
140 redmine_url,
141 api_key: api_key.to_string(),
142 impersonate_user_id: None,
143 })
144 }
145
146 pub fn from_env() -> Result<Self, crate::Error> {
156 let env_options = envy::from_env::<EnvOptions>()?;
157
158 let redmine_url = env_options.redmine_url;
159 let api_key = env_options.redmine_api_key;
160
161 Self::new(redmine_url, &api_key)
162 }
163
164 pub fn impersonate_user(&mut self, id: u64) {
168 self.impersonate_user_id = Some(id);
169 }
170
171 #[must_use]
176 #[allow(clippy::missing_panics_doc)]
177 pub fn issue_url(&self, issue_id: u64) -> Url {
178 let Redmine { redmine_url, .. } = self;
179 redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
182 }
183
184 fn rest(
187 &self,
188 method: reqwest::Method,
189 endpoint: &str,
190 parameters: QueryParams,
191 mime_type_and_body: Option<(&str, Vec<u8>)>,
192 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
193 let Redmine {
194 client,
195 redmine_url,
196 api_key,
197 impersonate_user_id,
198 } = self;
199 let mut url = redmine_url.join(endpoint)?;
200 parameters.add_to_url(&mut url);
201 debug!(%url, %method, "Calling redmine");
202 let req = client
203 .request(method.clone(), url.clone())
204 .header("x-redmine-api-key", api_key);
205 let req = if let Some(user_id) = impersonate_user_id {
206 req.header("X-Redmine-Switch-User", format!("{}", user_id))
207 } else {
208 req
209 };
210 let req = if let Some((mime, data)) = mime_type_and_body {
211 if let Ok(request_body) = from_utf8(&data) {
212 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
213 } else {
214 trace!(
215 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
216 mime,
217 data
218 );
219 }
220 req.body(data).header("Content-Type", mime)
221 } else {
222 req
223 };
224 let result = req.send();
225 if let Err(ref e) = result {
226 error!(%url, %method, "Redmine send error: {:?}", e);
227 }
228 let result = result?;
229 let status = result.status();
230 let response_body = result.bytes()?;
231 match from_utf8(&response_body) {
232 Ok(response_body) => {
233 trace!("Response body:\n{}", &response_body);
234 }
235 Err(e) => {
236 trace!(
237 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
238 &e,
239 &response_body
240 );
241 }
242 }
243 if status.is_client_error() {
244 error!(%url, %method, "Redmine status error (client error): {:?}", status);
245 } else if status.is_server_error() {
246 error!(%url, %method, "Redmine status error (server error): {:?}", status);
247 }
248 Ok((status, response_body))
249 }
250
251 pub fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
259 where
260 E: Endpoint,
261 {
262 let method = endpoint.method();
263 let url = endpoint.endpoint();
264 let parameters = endpoint.parameters();
265 let mime_type_and_body = endpoint.body()?;
266 self.rest(method, &url, parameters, mime_type_and_body)?;
267 Ok(())
268 }
269
270 pub fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
278 where
279 E: Endpoint + ReturnsJsonResponse + NoPagination,
280 R: DeserializeOwned + std::fmt::Debug,
281 {
282 let method = endpoint.method();
283 let url = endpoint.endpoint();
284 let parameters = endpoint.parameters();
285 let mime_type_and_body = endpoint.body()?;
286 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
287 if response_body.is_empty() {
288 Err(crate::Error::EmptyResponseBody(status))
289 } else {
290 let result = serde_json::from_slice::<R>(&response_body);
291 if let Ok(ref parsed_response_body) = result {
292 trace!("Parsed response body:\n{:#?}", parsed_response_body);
293 }
294 Ok(result?)
295 }
296 }
297
298 pub fn json_response_body_page<E, R>(
306 &self,
307 endpoint: &E,
308 offset: u64,
309 limit: u64,
310 ) -> Result<ResponsePage<R>, crate::Error>
311 where
312 E: Endpoint + ReturnsJsonResponse + Pageable,
313 R: DeserializeOwned + std::fmt::Debug,
314 {
315 let method = endpoint.method();
316 let url = endpoint.endpoint();
317 let mut parameters = endpoint.parameters();
318 parameters.push("offset", offset);
319 parameters.push("limit", limit);
320 let mime_type_and_body = endpoint.body()?;
321 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
322 if response_body.is_empty() {
323 Err(crate::Error::EmptyResponseBody(status))
324 } else {
325 let json_value_response_body: serde_json::Value =
326 serde_json::from_slice(&response_body)?;
327 let json_object_response_body = json_value_response_body.as_object();
328 if let Some(json_object_response_body) = json_object_response_body {
329 let total_count = json_object_response_body
330 .get("total_count")
331 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
332 .as_u64()
333 .ok_or_else(|| {
334 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
335 })?;
336 let offset = json_object_response_body
337 .get("offset")
338 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
339 .as_u64()
340 .ok_or_else(|| {
341 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
342 })?;
343 let limit = json_object_response_body
344 .get("limit")
345 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
346 .as_u64()
347 .ok_or_else(|| {
348 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
349 })?;
350 let response_wrapper_key = endpoint.response_wrapper_key();
351 let inner_response_body = json_object_response_body
352 .get(&response_wrapper_key)
353 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
354 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
355 if let Ok(ref parsed_response_body) = result {
356 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
357 }
358 Ok(ResponsePage {
359 values: result?,
360 total_count,
361 offset,
362 limit,
363 })
364 } else {
365 Err(crate::Error::NonObjectResponseBody(status))
366 }
367 }
368 }
369
370 pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
380 where
381 E: Endpoint + ReturnsJsonResponse + Pageable,
382 R: DeserializeOwned + std::fmt::Debug,
383 {
384 let method = endpoint.method();
385 let url = endpoint.endpoint();
386 let mut offset = 0;
387 let limit = 100;
388 let mut total_results = vec![];
389 loop {
390 let mut page_parameters = endpoint.parameters();
391 page_parameters.push("offset", offset);
392 page_parameters.push("limit", limit);
393 let mime_type_and_body = endpoint.body()?;
394 let (status, response_body) =
395 self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
396 if response_body.is_empty() {
397 return Err(crate::Error::EmptyResponseBody(status));
398 }
399 let json_value_response_body: serde_json::Value =
400 serde_json::from_slice(&response_body)?;
401 let json_object_response_body = json_value_response_body.as_object();
402 if let Some(json_object_response_body) = json_object_response_body {
403 let total_count: u64 = json_object_response_body
404 .get("total_count")
405 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
406 .as_u64()
407 .ok_or_else(|| {
408 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
409 })?;
410 let response_offset: u64 = json_object_response_body
411 .get("offset")
412 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
413 .as_u64()
414 .ok_or_else(|| {
415 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
416 })?;
417 let response_limit: u64 = json_object_response_body
418 .get("limit")
419 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
420 .as_u64()
421 .ok_or_else(|| {
422 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
423 })?;
424 let response_wrapper_key = endpoint.response_wrapper_key();
425 let inner_response_body = json_object_response_body
426 .get(&response_wrapper_key)
427 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
428 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
429 if let Ok(ref parsed_response_body) = result {
430 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
431 }
432 total_results.extend(result?);
433 if total_count < (response_offset + response_limit) {
434 break;
435 }
436 offset += limit;
437 } else {
438 return Err(crate::Error::NonObjectResponseBody(status));
439 }
440 }
441 Ok(total_results)
442 }
443}
444
445impl RedmineAsync {
446 pub fn new(redmine_url: url::Url, api_key: &str) -> Result<Self, crate::Error> {
452 #[cfg(not(feature = "rustls-tls"))]
453 let client = reqwest::Client::new();
454 #[cfg(feature = "rustls-tls")]
455 let client = reqwest::Client::builder().use_rustls_tls().build()?;
456
457 Ok(Self {
458 client,
459 redmine_url,
460 api_key: api_key.to_string(),
461 impersonate_user_id: None,
462 })
463 }
464
465 pub fn from_env() -> Result<Self, crate::Error> {
475 let env_options = envy::from_env::<EnvOptions>()?;
476
477 let redmine_url = env_options.redmine_url;
478 let api_key = env_options.redmine_api_key;
479
480 Self::new(redmine_url, &api_key)
481 }
482
483 pub fn impersonate_user(&mut self, id: u64) {
487 self.impersonate_user_id = Some(id);
488 }
489
490 #[must_use]
495 #[allow(clippy::missing_panics_doc)]
496 pub fn issue_url(&self, issue_id: u64) -> Url {
497 let RedmineAsync { redmine_url, .. } = self;
498 redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
501 }
502
503 async fn rest(
506 &self,
507 method: reqwest::Method,
508 endpoint: &str,
509 parameters: QueryParams<'_>,
510 mime_type_and_body: Option<(&str, Vec<u8>)>,
511 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
512 let RedmineAsync {
513 client,
514 redmine_url,
515 api_key,
516 impersonate_user_id,
517 } = self;
518 let mut url = redmine_url.join(endpoint)?;
519 parameters.add_to_url(&mut url);
520 debug!(%url, %method, "Calling redmine");
521 let req = client
522 .request(method.clone(), url.clone())
523 .header("x-redmine-api-key", api_key);
524 let req = if let Some(user_id) = impersonate_user_id {
525 req.header("X-Redmine-Switch-User", format!("{}", user_id))
526 } else {
527 req
528 };
529 let req = if let Some((mime, data)) = mime_type_and_body {
530 if let Ok(request_body) = from_utf8(&data) {
531 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
532 } else {
533 trace!(
534 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
535 mime,
536 data
537 );
538 }
539 req.body(data).header("Content-Type", mime)
540 } else {
541 req
542 };
543 let result = req.send().await;
544 if let Err(ref e) = result {
545 error!(%url, %method, "Redmine send error: {:?}", e);
546 }
547 let result = result?;
548 let status = result.status();
549 let response_body = result.bytes().await?;
550 match from_utf8(&response_body) {
551 Ok(response_body) => {
552 trace!("Response body:\n{}", &response_body);
553 }
554 Err(e) => {
555 trace!(
556 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
557 &e,
558 &response_body
559 );
560 }
561 }
562 if status.is_client_error() {
563 error!(%url, %method, "Redmine status error (client error): {:?}", status);
564 } else if status.is_server_error() {
565 error!(%url, %method, "Redmine status error (server error): {:?}", status);
566 }
567 Ok((status, response_body))
568 }
569
570 pub async fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
578 where
579 E: Endpoint,
580 {
581 let method = endpoint.method();
582 let url = endpoint.endpoint();
583 let parameters = endpoint.parameters();
584 let mime_type_and_body = endpoint.body()?;
585 self.rest(method, &url, parameters, mime_type_and_body)
586 .await?;
587 Ok(())
588 }
589
590 pub async fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
600 where
601 E: Endpoint + ReturnsJsonResponse + NoPagination,
602 R: DeserializeOwned + std::fmt::Debug,
603 {
604 let method = endpoint.method();
605 let url = endpoint.endpoint();
606 let parameters = endpoint.parameters();
607 let mime_type_and_body = endpoint.body()?;
608 let (status, response_body) = self
609 .rest(method, &url, parameters, mime_type_and_body)
610 .await?;
611 if response_body.is_empty() {
612 Err(crate::Error::EmptyResponseBody(status))
613 } else {
614 let result = serde_json::from_slice::<R>(&response_body);
615 if let Ok(ref parsed_response_body) = result {
616 trace!("Parsed response body:\n{:#?}", parsed_response_body);
617 }
618 Ok(result?)
619 }
620 }
621
622 pub async fn json_response_body_page<E, R>(
630 &self,
631 endpoint: &E,
632 offset: u64,
633 limit: u64,
634 ) -> Result<ResponsePage<R>, crate::Error>
635 where
636 E: Endpoint + ReturnsJsonResponse + Pageable,
637 R: DeserializeOwned + std::fmt::Debug,
638 {
639 let method = endpoint.method();
640 let url = endpoint.endpoint();
641 let mut parameters = endpoint.parameters();
642 parameters.push("offset", offset);
643 parameters.push("limit", limit);
644 let mime_type_and_body = endpoint.body()?;
645 let (status, response_body) = self
646 .rest(method, &url, parameters, mime_type_and_body)
647 .await?;
648 if response_body.is_empty() {
649 Err(crate::Error::EmptyResponseBody(status))
650 } else {
651 let json_value_response_body: serde_json::Value =
652 serde_json::from_slice(&response_body)?;
653 let json_object_response_body = json_value_response_body.as_object();
654 if let Some(json_object_response_body) = json_object_response_body {
655 let total_count = json_object_response_body
656 .get("total_count")
657 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
658 .as_u64()
659 .ok_or_else(|| {
660 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
661 })?;
662 let offset = json_object_response_body
663 .get("offset")
664 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
665 .as_u64()
666 .ok_or_else(|| {
667 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
668 })?;
669 let limit = json_object_response_body
670 .get("limit")
671 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
672 .as_u64()
673 .ok_or_else(|| {
674 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
675 })?;
676 let response_wrapper_key = endpoint.response_wrapper_key();
677 let inner_response_body = json_object_response_body
678 .get(&response_wrapper_key)
679 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
680 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
681 if let Ok(ref parsed_response_body) = result {
682 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
683 }
684 Ok(ResponsePage {
685 values: result?,
686 total_count,
687 offset,
688 limit,
689 })
690 } else {
691 Err(crate::Error::NonObjectResponseBody(status))
692 }
693 }
694 }
695
696 pub async fn json_response_body_all_pages<E, R>(
706 &self,
707 endpoint: &E,
708 ) -> Result<Vec<R>, crate::Error>
709 where
710 E: Endpoint + ReturnsJsonResponse + Pageable,
711 R: DeserializeOwned + std::fmt::Debug,
712 {
713 let method = endpoint.method();
714 let url = endpoint.endpoint();
715 let mut offset = 0;
716 let limit = 100;
717 let mut total_results = vec![];
718 loop {
719 let mut page_parameters = endpoint.parameters();
720 page_parameters.push("offset", offset);
721 page_parameters.push("limit", limit);
722 let mime_type_and_body = endpoint.body()?;
723 let (status, response_body) = self
724 .rest(method.clone(), &url, page_parameters, mime_type_and_body)
725 .await?;
726 if response_body.is_empty() {
727 return Err(crate::Error::EmptyResponseBody(status));
728 }
729 let json_value_response_body: serde_json::Value =
730 serde_json::from_slice(&response_body)?;
731 let json_object_response_body = json_value_response_body.as_object();
732 if let Some(json_object_response_body) = json_object_response_body {
733 let total_count: u64 = json_object_response_body
734 .get("total_count")
735 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
736 .as_u64()
737 .ok_or_else(|| {
738 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
739 })?;
740 let response_offset: u64 = json_object_response_body
741 .get("offset")
742 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
743 .as_u64()
744 .ok_or_else(|| {
745 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
746 })?;
747 let response_limit: u64 = json_object_response_body
748 .get("limit")
749 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
750 .as_u64()
751 .ok_or_else(|| {
752 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
753 })?;
754 let response_wrapper_key = endpoint.response_wrapper_key();
755 let inner_response_body = json_object_response_body
756 .get(&response_wrapper_key)
757 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
758 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
759 if let Ok(ref parsed_response_body) = result {
760 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
761 }
762 total_results.extend(result?);
763 if total_count < (response_offset + response_limit) {
764 break;
765 }
766 offset += limit;
767 } else {
768 return Err(crate::Error::NonObjectResponseBody(status));
769 }
770 }
771 Ok(total_results)
772 }
773}
774
775pub trait ParamValue<'a> {
777 #[allow(clippy::wrong_self_convention)]
778 fn as_value(&self) -> Cow<'a, str>;
780}
781
782impl ParamValue<'static> for bool {
783 fn as_value(&self) -> Cow<'static, str> {
784 if *self {
785 "true".into()
786 } else {
787 "false".into()
788 }
789 }
790}
791
792impl<'a> ParamValue<'a> for &'a str {
793 fn as_value(&self) -> Cow<'a, str> {
794 (*self).into()
795 }
796}
797
798impl ParamValue<'static> for String {
799 fn as_value(&self) -> Cow<'static, str> {
800 self.clone().into()
801 }
802}
803
804impl<'a> ParamValue<'a> for &'a String {
805 fn as_value(&self) -> Cow<'a, str> {
806 (*self).into()
807 }
808}
809
810impl<T> ParamValue<'static> for Vec<T>
813where
814 T: ToString,
815{
816 fn as_value(&self) -> Cow<'static, str> {
817 self.iter()
818 .map(|e| e.to_string())
819 .collect::<Vec<_>>()
820 .join(",")
821 .into()
822 }
823}
824
825impl<'a, T> ParamValue<'a> for &'a Vec<T>
828where
829 T: ToString,
830{
831 fn as_value(&self) -> Cow<'a, str> {
832 self.iter()
833 .map(|e| e.to_string())
834 .collect::<Vec<_>>()
835 .join(",")
836 .into()
837 }
838}
839
840impl<'a> ParamValue<'a> for Cow<'a, str> {
841 fn as_value(&self) -> Cow<'a, str> {
842 self.clone()
843 }
844}
845
846impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
847 fn as_value(&self) -> Cow<'a, str> {
848 (*self).clone()
849 }
850}
851
852impl ParamValue<'static> for u64 {
853 fn as_value(&self) -> Cow<'static, str> {
854 format!("{}", self).into()
855 }
856}
857
858impl ParamValue<'static> for f64 {
859 fn as_value(&self) -> Cow<'static, str> {
860 format!("{}", self).into()
861 }
862}
863
864impl ParamValue<'static> for time::OffsetDateTime {
865 fn as_value(&self) -> Cow<'static, str> {
866 self.format(&time::format_description::well_known::Rfc3339)
867 .unwrap()
868 .into()
869 }
870}
871
872impl ParamValue<'static> for time::Date {
873 fn as_value(&self) -> Cow<'static, str> {
874 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
875 self.format(&format).unwrap().into()
876 }
877}
878
879#[derive(Debug, Default, Clone)]
881pub struct QueryParams<'a> {
882 params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
884}
885
886impl<'a> QueryParams<'a> {
887 pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
889 where
890 K: Into<Cow<'a, str>>,
891 V: ParamValue<'b>,
892 'b: 'a,
893 {
894 self.params.push((key.into(), value.as_value()));
895 self
896 }
897
898 pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
900 where
901 K: Into<Cow<'a, str>>,
902 V: ParamValue<'b>,
903 'b: 'a,
904 {
905 if let Some(value) = value {
906 self.params.push((key.into(), value.as_value()));
907 }
908 self
909 }
910
911 pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
913 where
914 I: Iterator<Item = (K, V)>,
915 K: Into<Cow<'a, str>>,
916 V: ParamValue<'b>,
917 'b: 'a,
918 {
919 self.params
920 .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
921 self
922 }
923
924 pub fn add_to_url(&self, url: &mut Url) {
926 let mut pairs = url.query_pairs_mut();
927 pairs.extend_pairs(self.params.iter());
928 }
929}
930
931pub trait Endpoint {
933 fn method(&self) -> Method;
935 fn endpoint(&self) -> Cow<'static, str>;
937
938 fn parameters(&self) -> QueryParams {
940 QueryParams::default()
941 }
942
943 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
951 Ok(None)
952 }
953}
954
955pub trait ReturnsJsonResponse {}
957
958#[diagnostic::on_unimplemented(
962 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)`"
963)]
964pub trait NoPagination {}
965
966#[diagnostic::on_unimplemented(
968 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)`"
969)]
970pub trait Pageable {
971 fn response_wrapper_key(&self) -> String;
973}
974
975pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
983where
984 D: serde::Deserializer<'de>,
985{
986 let s = String::deserialize(deserializer)?;
987
988 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
989 .map_err(serde::de::Error::custom)
990}
991
992pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
1000where
1001 S: serde::Serializer,
1002{
1003 let s = t
1004 .format(&time::format_description::well_known::Rfc3339)
1005 .map_err(serde::ser::Error::custom)?;
1006
1007 s.serialize(serializer)
1008}
1009
1010pub fn deserialize_optional_rfc3339<'de, D>(
1018 deserializer: D,
1019) -> Result<Option<time::OffsetDateTime>, D::Error>
1020where
1021 D: serde::Deserializer<'de>,
1022{
1023 let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1024
1025 if let Some(s) = s {
1026 Ok(Some(
1027 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1028 .map_err(serde::de::Error::custom)?,
1029 ))
1030 } else {
1031 Ok(None)
1032 }
1033}
1034
1035pub fn serialize_optional_rfc3339<S>(
1043 t: &Option<time::OffsetDateTime>,
1044 serializer: S,
1045) -> Result<S::Ok, S::Error>
1046where
1047 S: serde::Serializer,
1048{
1049 if let Some(t) = t {
1050 let s = t
1051 .format(&time::format_description::well_known::Rfc3339)
1052 .map_err(serde::ser::Error::custom)?;
1053
1054 s.serialize(serializer)
1055 } else {
1056 let n: Option<String> = None;
1057 n.serialize(serializer)
1058 }
1059}