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>
280 where
281 E: Endpoint + ReturnsJsonResponse,
282 R: DeserializeOwned + std::fmt::Debug,
283 {
284 let method = endpoint.method();
285 let url = endpoint.endpoint();
286 let parameters = endpoint.parameters();
287 let mime_type_and_body = endpoint.body()?;
288 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
289 if response_body.is_empty() {
290 Err(crate::Error::EmptyResponseBody(status))
291 } else {
292 let result = serde_json::from_slice::<R>(&response_body);
293 if let Ok(ref parsed_response_body) = result {
294 trace!("Parsed response body:\n{:#?}", parsed_response_body);
295 }
296 Ok(result?)
297 }
298 }
299
300 pub fn json_response_body_page<E, R>(
308 &self,
309 endpoint: &E,
310 offset: u64,
311 limit: u64,
312 ) -> Result<ResponsePage<R>, crate::Error>
313 where
314 E: Endpoint + ReturnsJsonResponse + Pageable,
315 R: DeserializeOwned + std::fmt::Debug,
316 {
317 let method = endpoint.method();
318 let url = endpoint.endpoint();
319 let mut parameters = endpoint.parameters();
320 parameters.push("offset", offset);
321 parameters.push("limit", limit);
322 let mime_type_and_body = endpoint.body()?;
323 let (status, response_body) = self.rest(method, &url, parameters, mime_type_and_body)?;
324 if response_body.is_empty() {
325 Err(crate::Error::EmptyResponseBody(status))
326 } else {
327 let json_value_response_body: serde_json::Value =
328 serde_json::from_slice(&response_body)?;
329 let json_object_response_body = json_value_response_body.as_object();
330 if let Some(json_object_response_body) = json_object_response_body {
331 let total_count = json_object_response_body
332 .get("total_count")
333 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
334 .as_u64()
335 .ok_or_else(|| {
336 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
337 })?;
338 let offset = json_object_response_body
339 .get("offset")
340 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
341 .as_u64()
342 .ok_or_else(|| {
343 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
344 })?;
345 let limit = json_object_response_body
346 .get("limit")
347 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
348 .as_u64()
349 .ok_or_else(|| {
350 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
351 })?;
352 let response_wrapper_key = endpoint.response_wrapper_key();
353 let inner_response_body = json_object_response_body
354 .get(&response_wrapper_key)
355 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
356 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
357 if let Ok(ref parsed_response_body) = result {
358 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
359 }
360 Ok(ResponsePage {
361 values: result?,
362 total_count,
363 offset,
364 limit,
365 })
366 } else {
367 Err(crate::Error::NonObjectResponseBody(status))
368 }
369 }
370 }
371
372 pub fn json_response_body_all_pages<E, R>(&self, endpoint: &E) -> Result<Vec<R>, crate::Error>
382 where
383 E: Endpoint + ReturnsJsonResponse + Pageable,
384 R: DeserializeOwned + std::fmt::Debug,
385 {
386 let method = endpoint.method();
387 let url = endpoint.endpoint();
388 let mut offset = 0;
389 let limit = 100;
390 let mut total_results = vec![];
391 loop {
392 let mut page_parameters = endpoint.parameters();
393 page_parameters.push("offset", offset);
394 page_parameters.push("limit", limit);
395 let mime_type_and_body = endpoint.body()?;
396 let (status, response_body) =
397 self.rest(method.clone(), &url, page_parameters, mime_type_and_body)?;
398 if response_body.is_empty() {
399 return Err(crate::Error::EmptyResponseBody(status));
400 }
401 let json_value_response_body: serde_json::Value =
402 serde_json::from_slice(&response_body)?;
403 let json_object_response_body = json_value_response_body.as_object();
404 if let Some(json_object_response_body) = json_object_response_body {
405 let total_count: u64 = json_object_response_body
406 .get("total_count")
407 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
408 .as_u64()
409 .ok_or_else(|| {
410 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
411 })?;
412 let response_offset: u64 = json_object_response_body
413 .get("offset")
414 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
415 .as_u64()
416 .ok_or_else(|| {
417 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
418 })?;
419 let response_limit: u64 = json_object_response_body
420 .get("limit")
421 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
422 .as_u64()
423 .ok_or_else(|| {
424 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
425 })?;
426 let response_wrapper_key = endpoint.response_wrapper_key();
427 let inner_response_body = json_object_response_body
428 .get(&response_wrapper_key)
429 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
430 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
431 if let Ok(ref parsed_response_body) = result {
432 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
433 }
434 total_results.extend(result?);
435 if total_count < (response_offset + response_limit) {
436 break;
437 }
438 offset += limit;
439 } else {
440 return Err(crate::Error::NonObjectResponseBody(status));
441 }
442 }
443 Ok(total_results)
444 }
445}
446
447impl RedmineAsync {
448 pub fn new(redmine_url: url::Url, api_key: &str) -> Result<Self, crate::Error> {
454 #[cfg(not(feature = "rustls-tls"))]
455 let client = reqwest::Client::new();
456 #[cfg(feature = "rustls-tls")]
457 let client = reqwest::Client::builder().use_rustls_tls().build()?;
458
459 Ok(Self {
460 client,
461 redmine_url,
462 api_key: api_key.to_string(),
463 impersonate_user_id: None,
464 })
465 }
466
467 pub fn from_env() -> Result<Self, crate::Error> {
477 let env_options = envy::from_env::<EnvOptions>()?;
478
479 let redmine_url = env_options.redmine_url;
480 let api_key = env_options.redmine_api_key;
481
482 Self::new(redmine_url, &api_key)
483 }
484
485 pub fn impersonate_user(&mut self, id: u64) {
489 self.impersonate_user_id = Some(id);
490 }
491
492 #[must_use]
497 #[allow(clippy::missing_panics_doc)]
498 pub fn issue_url(&self, issue_id: u64) -> Url {
499 let RedmineAsync { redmine_url, .. } = self;
500 redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
503 }
504
505 async fn rest(
508 &self,
509 method: reqwest::Method,
510 endpoint: &str,
511 parameters: QueryParams<'_>,
512 mime_type_and_body: Option<(&str, Vec<u8>)>,
513 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
514 let RedmineAsync {
515 client,
516 redmine_url,
517 api_key,
518 impersonate_user_id,
519 } = self;
520 let mut url = redmine_url.join(endpoint)?;
521 parameters.add_to_url(&mut url);
522 debug!(%url, %method, "Calling redmine");
523 let req = client
524 .request(method.clone(), url.clone())
525 .header("x-redmine-api-key", api_key);
526 let req = if let Some(user_id) = impersonate_user_id {
527 req.header("X-Redmine-Switch-User", format!("{}", user_id))
528 } else {
529 req
530 };
531 let req = if let Some((mime, data)) = mime_type_and_body {
532 if let Ok(request_body) = from_utf8(&data) {
533 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
534 } else {
535 trace!(
536 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
537 mime,
538 data
539 );
540 }
541 req.body(data).header("Content-Type", mime)
542 } else {
543 req
544 };
545 let result = req.send().await;
546 if let Err(ref e) = result {
547 error!(%url, %method, "Redmine send error: {:?}", e);
548 }
549 let result = result?;
550 let status = result.status();
551 let response_body = result.bytes().await?;
552 match from_utf8(&response_body) {
553 Ok(response_body) => {
554 trace!("Response body:\n{}", &response_body);
555 }
556 Err(e) => {
557 trace!(
558 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
559 &e,
560 &response_body
561 );
562 }
563 }
564 if status.is_client_error() {
565 error!(%url, %method, "Redmine status error (client error): {:?}", status);
566 } else if status.is_server_error() {
567 error!(%url, %method, "Redmine status error (server error): {:?}", status);
568 }
569 Ok((status, response_body))
570 }
571
572 pub async fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
580 where
581 E: Endpoint,
582 {
583 let method = endpoint.method();
584 let url = endpoint.endpoint();
585 let parameters = endpoint.parameters();
586 let mime_type_and_body = endpoint.body()?;
587 self.rest(method, &url, parameters, mime_type_and_body)
588 .await?;
589 Ok(())
590 }
591
592 pub async fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
602 where
603 E: Endpoint + ReturnsJsonResponse,
604 R: DeserializeOwned + std::fmt::Debug,
605 {
606 let method = endpoint.method();
607 let url = endpoint.endpoint();
608 let parameters = endpoint.parameters();
609 let mime_type_and_body = endpoint.body()?;
610 let (status, response_body) = self
611 .rest(method, &url, parameters, mime_type_and_body)
612 .await?;
613 if response_body.is_empty() {
614 Err(crate::Error::EmptyResponseBody(status))
615 } else {
616 let result = serde_json::from_slice::<R>(&response_body);
617 if let Ok(ref parsed_response_body) = result {
618 trace!("Parsed response body:\n{:#?}", parsed_response_body);
619 }
620 Ok(result?)
621 }
622 }
623
624 pub async fn json_response_body_page<E, R>(
632 &self,
633 endpoint: &E,
634 offset: u64,
635 limit: u64,
636 ) -> Result<ResponsePage<R>, crate::Error>
637 where
638 E: Endpoint + ReturnsJsonResponse + Pageable,
639 R: DeserializeOwned + std::fmt::Debug,
640 {
641 let method = endpoint.method();
642 let url = endpoint.endpoint();
643 let mut parameters = endpoint.parameters();
644 parameters.push("offset", offset);
645 parameters.push("limit", limit);
646 let mime_type_and_body = endpoint.body()?;
647 let (status, response_body) = self
648 .rest(method, &url, parameters, mime_type_and_body)
649 .await?;
650 if response_body.is_empty() {
651 Err(crate::Error::EmptyResponseBody(status))
652 } else {
653 let json_value_response_body: serde_json::Value =
654 serde_json::from_slice(&response_body)?;
655 let json_object_response_body = json_value_response_body.as_object();
656 if let Some(json_object_response_body) = json_object_response_body {
657 let total_count = json_object_response_body
658 .get("total_count")
659 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
660 .as_u64()
661 .ok_or_else(|| {
662 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
663 })?;
664 let offset = json_object_response_body
665 .get("offset")
666 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
667 .as_u64()
668 .ok_or_else(|| {
669 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
670 })?;
671 let limit = json_object_response_body
672 .get("limit")
673 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
674 .as_u64()
675 .ok_or_else(|| {
676 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
677 })?;
678 let response_wrapper_key = endpoint.response_wrapper_key();
679 let inner_response_body = json_object_response_body
680 .get(&response_wrapper_key)
681 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
682 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
683 if let Ok(ref parsed_response_body) = result {
684 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
685 }
686 Ok(ResponsePage {
687 values: result?,
688 total_count,
689 offset,
690 limit,
691 })
692 } else {
693 Err(crate::Error::NonObjectResponseBody(status))
694 }
695 }
696 }
697
698 pub async fn json_response_body_all_pages<E, R>(
708 &self,
709 endpoint: &E,
710 ) -> Result<Vec<R>, crate::Error>
711 where
712 E: Endpoint + ReturnsJsonResponse + Pageable,
713 R: DeserializeOwned + std::fmt::Debug,
714 {
715 let method = endpoint.method();
716 let url = endpoint.endpoint();
717 let mut offset = 0;
718 let limit = 100;
719 let mut total_results = vec![];
720 loop {
721 let mut page_parameters = endpoint.parameters();
722 page_parameters.push("offset", offset);
723 page_parameters.push("limit", limit);
724 let mime_type_and_body = endpoint.body()?;
725 let (status, response_body) = self
726 .rest(method.clone(), &url, page_parameters, mime_type_and_body)
727 .await?;
728 if response_body.is_empty() {
729 return Err(crate::Error::EmptyResponseBody(status));
730 }
731 let json_value_response_body: serde_json::Value =
732 serde_json::from_slice(&response_body)?;
733 let json_object_response_body = json_value_response_body.as_object();
734 if let Some(json_object_response_body) = json_object_response_body {
735 let total_count: u64 = json_object_response_body
736 .get("total_count")
737 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
738 .as_u64()
739 .ok_or_else(|| {
740 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
741 })?;
742 let response_offset: u64 = json_object_response_body
743 .get("offset")
744 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
745 .as_u64()
746 .ok_or_else(|| {
747 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
748 })?;
749 let response_limit: u64 = json_object_response_body
750 .get("limit")
751 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
752 .as_u64()
753 .ok_or_else(|| {
754 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
755 })?;
756 let response_wrapper_key = endpoint.response_wrapper_key();
757 let inner_response_body = json_object_response_body
758 .get(&response_wrapper_key)
759 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
760 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
761 if let Ok(ref parsed_response_body) = result {
762 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
763 }
764 total_results.extend(result?);
765 if total_count < (response_offset + response_limit) {
766 break;
767 }
768 offset += limit;
769 } else {
770 return Err(crate::Error::NonObjectResponseBody(status));
771 }
772 }
773 Ok(total_results)
774 }
775}
776
777pub trait ParamValue<'a> {
779 #[allow(clippy::wrong_self_convention)]
780 fn as_value(&self) -> Cow<'a, str>;
782}
783
784impl ParamValue<'static> for bool {
785 fn as_value(&self) -> Cow<'static, str> {
786 if *self {
787 "true".into()
788 } else {
789 "false".into()
790 }
791 }
792}
793
794impl<'a> ParamValue<'a> for &'a str {
795 fn as_value(&self) -> Cow<'a, str> {
796 (*self).into()
797 }
798}
799
800impl ParamValue<'static> for String {
801 fn as_value(&self) -> Cow<'static, str> {
802 self.clone().into()
803 }
804}
805
806impl<'a> ParamValue<'a> for &'a String {
807 fn as_value(&self) -> Cow<'a, str> {
808 (*self).into()
809 }
810}
811
812impl<T> ParamValue<'static> for Vec<T>
815where
816 T: ToString,
817{
818 fn as_value(&self) -> Cow<'static, str> {
819 self.iter()
820 .map(|e| e.to_string())
821 .collect::<Vec<_>>()
822 .join(",")
823 .into()
824 }
825}
826
827impl<'a, T> ParamValue<'a> for &'a Vec<T>
830where
831 T: ToString,
832{
833 fn as_value(&self) -> Cow<'a, str> {
834 self.iter()
835 .map(|e| e.to_string())
836 .collect::<Vec<_>>()
837 .join(",")
838 .into()
839 }
840}
841
842impl<'a> ParamValue<'a> for Cow<'a, str> {
843 fn as_value(&self) -> Cow<'a, str> {
844 self.clone()
845 }
846}
847
848impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
849 fn as_value(&self) -> Cow<'a, str> {
850 (*self).clone()
851 }
852}
853
854impl ParamValue<'static> for u64 {
855 fn as_value(&self) -> Cow<'static, str> {
856 format!("{}", self).into()
857 }
858}
859
860impl ParamValue<'static> for f64 {
861 fn as_value(&self) -> Cow<'static, str> {
862 format!("{}", self).into()
863 }
864}
865
866impl ParamValue<'static> for time::OffsetDateTime {
867 fn as_value(&self) -> Cow<'static, str> {
868 self.format(&time::format_description::well_known::Rfc3339)
869 .unwrap()
870 .into()
871 }
872}
873
874impl ParamValue<'static> for time::Date {
875 fn as_value(&self) -> Cow<'static, str> {
876 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
877 self.format(&format).unwrap().into()
878 }
879}
880
881#[derive(Debug, Default, Clone)]
883pub struct QueryParams<'a> {
884 params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
886}
887
888impl<'a> QueryParams<'a> {
889 pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
891 where
892 K: Into<Cow<'a, str>>,
893 V: ParamValue<'b>,
894 'b: 'a,
895 {
896 self.params.push((key.into(), value.as_value()));
897 self
898 }
899
900 pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
902 where
903 K: Into<Cow<'a, str>>,
904 V: ParamValue<'b>,
905 'b: 'a,
906 {
907 if let Some(value) = value {
908 self.params.push((key.into(), value.as_value()));
909 }
910 self
911 }
912
913 pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
915 where
916 I: Iterator<Item = (K, V)>,
917 K: Into<Cow<'a, str>>,
918 V: ParamValue<'b>,
919 'b: 'a,
920 {
921 self.params
922 .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
923 self
924 }
925
926 pub fn add_to_url(&self, url: &mut Url) {
928 let mut pairs = url.query_pairs_mut();
929 pairs.extend_pairs(self.params.iter());
930 }
931}
932
933pub trait Endpoint {
935 fn method(&self) -> Method;
937 fn endpoint(&self) -> Cow<'static, str>;
939
940 fn parameters(&self) -> QueryParams {
942 QueryParams::default()
943 }
944
945 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
953 Ok(None)
954 }
955}
956
957pub trait ReturnsJsonResponse {}
959
960pub trait Pageable {
962 fn response_wrapper_key(&self) -> String;
964}
965
966pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
974where
975 D: serde::Deserializer<'de>,
976{
977 let s = String::deserialize(deserializer)?;
978
979 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
980 .map_err(serde::de::Error::custom)
981}
982
983pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
991where
992 S: serde::Serializer,
993{
994 let s = t
995 .format(&time::format_description::well_known::Rfc3339)
996 .map_err(serde::ser::Error::custom)?;
997
998 s.serialize(serializer)
999}
1000
1001pub fn deserialize_optional_rfc3339<'de, D>(
1009 deserializer: D,
1010) -> Result<Option<time::OffsetDateTime>, D::Error>
1011where
1012 D: serde::Deserializer<'de>,
1013{
1014 let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1015
1016 if let Some(s) = s {
1017 Ok(Some(
1018 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1019 .map_err(serde::de::Error::custom)?,
1020 ))
1021 } else {
1022 Ok(None)
1023 }
1024}
1025
1026pub fn serialize_optional_rfc3339<S>(
1034 t: &Option<time::OffsetDateTime>,
1035 serializer: S,
1036) -> Result<S::Ok, S::Error>
1037where
1038 S: serde::Serializer,
1039{
1040 if let Some(t) = t {
1041 let s = t
1042 .format(&time::format_description::well_known::Rfc3339)
1043 .map_err(serde::ser::Error::custom)?;
1044
1045 s.serialize(serializer)
1046 } else {
1047 let n: Option<String> = None;
1048 n.serialize(serializer)
1049 }
1050}