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(
131 client: reqwest::blocking::Client,
132 redmine_url: url::Url,
133 api_key: &str,
134 ) -> Result<Self, crate::Error> {
135 Ok(Self {
136 client,
137 redmine_url,
138 api_key: api_key.to_string(),
139 impersonate_user_id: None,
140 })
141 }
142
143 pub fn from_env(client: reqwest::blocking::Client) -> Result<Self, crate::Error> {
153 let env_options = envy::from_env::<EnvOptions>()?;
154
155 let redmine_url = env_options.redmine_url;
156 let api_key = env_options.redmine_api_key;
157
158 Self::new(client, redmine_url, &api_key)
159 }
160
161 pub fn impersonate_user(&mut self, id: u64) {
165 self.impersonate_user_id = Some(id);
166 }
167
168 #[must_use]
173 #[allow(clippy::missing_panics_doc)]
174 pub fn issue_url(&self, issue_id: u64) -> Url {
175 let Redmine { redmine_url, .. } = self;
176 redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
179 }
180
181 fn rest(
184 &self,
185 method: reqwest::Method,
186 endpoint: &str,
187 parameters: QueryParams,
188 mime_type_and_body: Option<(&str, Vec<u8>)>,
189 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
190 let Redmine {
191 client,
192 redmine_url,
193 api_key,
194 impersonate_user_id,
195 } = self;
196 let mut url = redmine_url.join(endpoint)?;
197 parameters.add_to_url(&mut url);
198 debug!(%url, %method, "Calling redmine");
199 let req = client
200 .request(method.clone(), url.clone())
201 .header("x-redmine-api-key", api_key);
202 let req = if let Some(user_id) = impersonate_user_id {
203 req.header("X-Redmine-Switch-User", format!("{}", user_id))
204 } else {
205 req
206 };
207 let req = if let Some((mime, data)) = mime_type_and_body {
208 if let Ok(request_body) = from_utf8(&data) {
209 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
210 } else {
211 trace!(
212 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
213 mime,
214 data
215 );
216 }
217 req.body(data).header("Content-Type", mime)
218 } else {
219 req
220 };
221 let result = req.send();
222 if let Err(ref e) = result {
223 error!(%url, %method, "Redmine send error: {:?}", e);
224 }
225 let result = result?;
226 let status = result.status();
227 let response_body = result.bytes()?;
228 match from_utf8(&response_body) {
229 Ok(response_body) => {
230 trace!("Response body:\n{}", &response_body);
231 }
232 Err(e) => {
233 trace!(
234 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
235 &e,
236 &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
442impl RedmineAsync {
443 pub fn new(
449 client: reqwest::Client,
450 redmine_url: url::Url,
451 api_key: &str,
452 ) -> Result<Self, crate::Error> {
453 Ok(Self {
454 client,
455 redmine_url,
456 api_key: api_key.to_string(),
457 impersonate_user_id: None,
458 })
459 }
460
461 pub fn from_env(client: reqwest::Client) -> Result<Self, crate::Error> {
471 let env_options = envy::from_env::<EnvOptions>()?;
472
473 let redmine_url = env_options.redmine_url;
474 let api_key = env_options.redmine_api_key;
475
476 Self::new(client, redmine_url, &api_key)
477 }
478
479 pub fn impersonate_user(&mut self, id: u64) {
483 self.impersonate_user_id = Some(id);
484 }
485
486 #[must_use]
491 #[allow(clippy::missing_panics_doc)]
492 pub fn issue_url(&self, issue_id: u64) -> Url {
493 let RedmineAsync { redmine_url, .. } = self;
494 redmine_url.join(&format!("/issues/{}", issue_id)).unwrap()
497 }
498
499 async fn rest(
502 &self,
503 method: reqwest::Method,
504 endpoint: &str,
505 parameters: QueryParams<'_>,
506 mime_type_and_body: Option<(&str, Vec<u8>)>,
507 ) -> Result<(reqwest::StatusCode, bytes::Bytes), crate::Error> {
508 let RedmineAsync {
509 client,
510 redmine_url,
511 api_key,
512 impersonate_user_id,
513 } = self;
514 let mut url = redmine_url.join(endpoint)?;
515 parameters.add_to_url(&mut url);
516 debug!(%url, %method, "Calling redmine");
517 let req = client
518 .request(method.clone(), url.clone())
519 .header("x-redmine-api-key", api_key);
520 let req = if let Some(user_id) = impersonate_user_id {
521 req.header("X-Redmine-Switch-User", format!("{}", user_id))
522 } else {
523 req
524 };
525 let req = if let Some((mime, data)) = mime_type_and_body {
526 if let Ok(request_body) = from_utf8(&data) {
527 trace!("Request body (Content-Type: {}):\n{}", mime, request_body);
528 } else {
529 trace!(
530 "Request body (Content-Type: {}) could not be parsed as UTF-8:\n{:?}",
531 mime,
532 data
533 );
534 }
535 req.body(data).header("Content-Type", mime)
536 } else {
537 req
538 };
539 let result = req.send().await;
540 if let Err(ref e) = result {
541 error!(%url, %method, "Redmine send error: {:?}", e);
542 }
543 let result = result?;
544 let status = result.status();
545 let response_body = result.bytes().await?;
546 match from_utf8(&response_body) {
547 Ok(response_body) => {
548 trace!("Response body:\n{}", &response_body);
549 }
550 Err(e) => {
551 trace!(
552 "Response body that could not be parsed as utf8 because of {}:\n{:?}",
553 &e,
554 &response_body
555 );
556 }
557 }
558 if status.is_client_error() {
559 error!(%url, %method, "Redmine status error (client error): {:?}", status);
560 } else if status.is_server_error() {
561 error!(%url, %method, "Redmine status error (server error): {:?}", status);
562 }
563 Ok((status, response_body))
564 }
565
566 pub async fn ignore_response_body<E>(&self, endpoint: &E) -> Result<(), crate::Error>
574 where
575 E: Endpoint,
576 {
577 let method = endpoint.method();
578 let url = endpoint.endpoint();
579 let parameters = endpoint.parameters();
580 let mime_type_and_body = endpoint.body()?;
581 self.rest(method, &url, parameters, mime_type_and_body)
582 .await?;
583 Ok(())
584 }
585
586 pub async fn json_response_body<E, R>(&self, endpoint: &E) -> Result<R, crate::Error>
596 where
597 E: Endpoint + ReturnsJsonResponse + NoPagination,
598 R: DeserializeOwned + std::fmt::Debug,
599 {
600 let method = endpoint.method();
601 let url = endpoint.endpoint();
602 let parameters = endpoint.parameters();
603 let mime_type_and_body = endpoint.body()?;
604 let (status, response_body) = self
605 .rest(method, &url, parameters, mime_type_and_body)
606 .await?;
607 if response_body.is_empty() {
608 Err(crate::Error::EmptyResponseBody(status))
609 } else {
610 let result = serde_json::from_slice::<R>(&response_body);
611 if let Ok(ref parsed_response_body) = result {
612 trace!("Parsed response body:\n{:#?}", parsed_response_body);
613 }
614 Ok(result?)
615 }
616 }
617
618 pub async fn json_response_body_page<E, R>(
626 &self,
627 endpoint: &E,
628 offset: u64,
629 limit: u64,
630 ) -> Result<ResponsePage<R>, crate::Error>
631 where
632 E: Endpoint + ReturnsJsonResponse + Pageable,
633 R: DeserializeOwned + std::fmt::Debug,
634 {
635 let method = endpoint.method();
636 let url = endpoint.endpoint();
637 let mut parameters = endpoint.parameters();
638 parameters.push("offset", offset);
639 parameters.push("limit", limit);
640 let mime_type_and_body = endpoint.body()?;
641 let (status, response_body) = self
642 .rest(method, &url, parameters, mime_type_and_body)
643 .await?;
644 if response_body.is_empty() {
645 Err(crate::Error::EmptyResponseBody(status))
646 } else {
647 let json_value_response_body: serde_json::Value =
648 serde_json::from_slice(&response_body)?;
649 let json_object_response_body = json_value_response_body.as_object();
650 if let Some(json_object_response_body) = json_object_response_body {
651 let total_count = json_object_response_body
652 .get("total_count")
653 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
654 .as_u64()
655 .ok_or_else(|| {
656 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
657 })?;
658 let offset = json_object_response_body
659 .get("offset")
660 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
661 .as_u64()
662 .ok_or_else(|| {
663 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
664 })?;
665 let limit = json_object_response_body
666 .get("limit")
667 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
668 .as_u64()
669 .ok_or_else(|| {
670 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
671 })?;
672 let response_wrapper_key = endpoint.response_wrapper_key();
673 let inner_response_body = json_object_response_body
674 .get(&response_wrapper_key)
675 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
676 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
677 if let Ok(ref parsed_response_body) = result {
678 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
679 }
680 Ok(ResponsePage {
681 values: result?,
682 total_count,
683 offset,
684 limit,
685 })
686 } else {
687 Err(crate::Error::NonObjectResponseBody(status))
688 }
689 }
690 }
691
692 pub async fn json_response_body_all_pages<E, R>(
702 &self,
703 endpoint: &E,
704 ) -> Result<Vec<R>, crate::Error>
705 where
706 E: Endpoint + ReturnsJsonResponse + Pageable,
707 R: DeserializeOwned + std::fmt::Debug,
708 {
709 let method = endpoint.method();
710 let url = endpoint.endpoint();
711 let mut offset = 0;
712 let limit = 100;
713 let mut total_results = vec![];
714 loop {
715 let mut page_parameters = endpoint.parameters();
716 page_parameters.push("offset", offset);
717 page_parameters.push("limit", limit);
718 let mime_type_and_body = endpoint.body()?;
719 let (status, response_body) = self
720 .rest(method.clone(), &url, page_parameters, mime_type_and_body)
721 .await?;
722 if response_body.is_empty() {
723 return Err(crate::Error::EmptyResponseBody(status));
724 }
725 let json_value_response_body: serde_json::Value =
726 serde_json::from_slice(&response_body)?;
727 let json_object_response_body = json_value_response_body.as_object();
728 if let Some(json_object_response_body) = json_object_response_body {
729 let total_count: u64 = json_object_response_body
730 .get("total_count")
731 .ok_or_else(|| crate::Error::PaginationKeyMissing("total_count".to_string()))?
732 .as_u64()
733 .ok_or_else(|| {
734 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
735 })?;
736 let response_offset: u64 = json_object_response_body
737 .get("offset")
738 .ok_or_else(|| crate::Error::PaginationKeyMissing("offset".to_string()))?
739 .as_u64()
740 .ok_or_else(|| {
741 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
742 })?;
743 let response_limit: u64 = json_object_response_body
744 .get("limit")
745 .ok_or_else(|| crate::Error::PaginationKeyMissing("limit".to_string()))?
746 .as_u64()
747 .ok_or_else(|| {
748 crate::Error::PaginationKeyHasWrongType("total_count".to_string())
749 })?;
750 let response_wrapper_key = endpoint.response_wrapper_key();
751 let inner_response_body = json_object_response_body
752 .get(&response_wrapper_key)
753 .ok_or(crate::Error::PaginationKeyMissing(response_wrapper_key))?;
754 let result = serde_json::from_value::<Vec<R>>(inner_response_body.to_owned());
755 if let Ok(ref parsed_response_body) = result {
756 trace!(%total_count, %offset, %limit, "Parsed response body:\n{:?}", parsed_response_body);
757 }
758 total_results.extend(result?);
759 if total_count < (response_offset + response_limit) {
760 break;
761 }
762 offset += limit;
763 } else {
764 return Err(crate::Error::NonObjectResponseBody(status));
765 }
766 }
767 Ok(total_results)
768 }
769}
770
771pub trait ParamValue<'a> {
773 #[allow(clippy::wrong_self_convention)]
774 fn as_value(&self) -> Cow<'a, str>;
776}
777
778impl ParamValue<'static> for bool {
779 fn as_value(&self) -> Cow<'static, str> {
780 if *self {
781 "true".into()
782 } else {
783 "false".into()
784 }
785 }
786}
787
788impl<'a> ParamValue<'a> for &'a str {
789 fn as_value(&self) -> Cow<'a, str> {
790 (*self).into()
791 }
792}
793
794impl ParamValue<'static> for String {
795 fn as_value(&self) -> Cow<'static, str> {
796 self.clone().into()
797 }
798}
799
800impl<'a> ParamValue<'a> for &'a String {
801 fn as_value(&self) -> Cow<'a, str> {
802 (*self).into()
803 }
804}
805
806impl<T> ParamValue<'static> for Vec<T>
809where
810 T: ToString,
811{
812 fn as_value(&self) -> Cow<'static, str> {
813 self.iter()
814 .map(|e| e.to_string())
815 .collect::<Vec<_>>()
816 .join(",")
817 .into()
818 }
819}
820
821impl<'a, T> ParamValue<'a> for &'a Vec<T>
824where
825 T: ToString,
826{
827 fn as_value(&self) -> Cow<'a, str> {
828 self.iter()
829 .map(|e| e.to_string())
830 .collect::<Vec<_>>()
831 .join(",")
832 .into()
833 }
834}
835
836impl<'a> ParamValue<'a> for Cow<'a, str> {
837 fn as_value(&self) -> Cow<'a, str> {
838 self.clone()
839 }
840}
841
842impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
843 fn as_value(&self) -> Cow<'a, str> {
844 (*self).clone()
845 }
846}
847
848impl ParamValue<'static> for u64 {
849 fn as_value(&self) -> Cow<'static, str> {
850 format!("{}", self).into()
851 }
852}
853
854impl ParamValue<'static> for f64 {
855 fn as_value(&self) -> Cow<'static, str> {
856 format!("{}", self).into()
857 }
858}
859
860impl ParamValue<'static> for time::OffsetDateTime {
861 fn as_value(&self) -> Cow<'static, str> {
862 self.format(&time::format_description::well_known::Rfc3339)
863 .unwrap()
864 .into()
865 }
866}
867
868impl ParamValue<'static> for time::Date {
869 fn as_value(&self) -> Cow<'static, str> {
870 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
871 self.format(&format).unwrap().into()
872 }
873}
874
875#[derive(Debug, Default, Clone)]
877pub struct QueryParams<'a> {
878 params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
880}
881
882impl<'a> QueryParams<'a> {
883 pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
885 where
886 K: Into<Cow<'a, str>>,
887 V: ParamValue<'b>,
888 'b: 'a,
889 {
890 self.params.push((key.into(), value.as_value()));
891 self
892 }
893
894 pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
896 where
897 K: Into<Cow<'a, str>>,
898 V: ParamValue<'b>,
899 'b: 'a,
900 {
901 if let Some(value) = value {
902 self.params.push((key.into(), value.as_value()));
903 }
904 self
905 }
906
907 pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
909 where
910 I: Iterator<Item = (K, V)>,
911 K: Into<Cow<'a, str>>,
912 V: ParamValue<'b>,
913 'b: 'a,
914 {
915 self.params
916 .extend(iter.map(|(key, value)| (key.into(), value.as_value())));
917 self
918 }
919
920 pub fn add_to_url(&self, url: &mut Url) {
922 let mut pairs = url.query_pairs_mut();
923 pairs.extend_pairs(self.params.iter());
924 }
925}
926
927pub trait Endpoint {
929 fn method(&self) -> Method;
931 fn endpoint(&self) -> Cow<'static, str>;
933
934 fn parameters(&self) -> QueryParams {
936 QueryParams::default()
937 }
938
939 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
947 Ok(None)
948 }
949}
950
951pub trait ReturnsJsonResponse {}
953
954#[diagnostic::on_unimplemented(
958 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)`"
959)]
960pub trait NoPagination {}
961
962#[diagnostic::on_unimplemented(
964 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)`"
965)]
966pub trait Pageable {
967 fn response_wrapper_key(&self) -> String;
969}
970
971pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
979where
980 D: serde::Deserializer<'de>,
981{
982 let s = String::deserialize(deserializer)?;
983
984 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
985 .map_err(serde::de::Error::custom)
986}
987
988pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
996where
997 S: serde::Serializer,
998{
999 let s = t
1000 .format(&time::format_description::well_known::Rfc3339)
1001 .map_err(serde::ser::Error::custom)?;
1002
1003 s.serialize(serializer)
1004}
1005
1006pub fn deserialize_optional_rfc3339<'de, D>(
1014 deserializer: D,
1015) -> Result<Option<time::OffsetDateTime>, D::Error>
1016where
1017 D: serde::Deserializer<'de>,
1018{
1019 let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
1020
1021 if let Some(s) = s {
1022 Ok(Some(
1023 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
1024 .map_err(serde::de::Error::custom)?,
1025 ))
1026 } else {
1027 Ok(None)
1028 }
1029}
1030
1031pub fn serialize_optional_rfc3339<S>(
1039 t: &Option<time::OffsetDateTime>,
1040 serializer: S,
1041) -> Result<S::Ok, S::Error>
1042where
1043 S: serde::Serializer,
1044{
1045 if let Some(t) = t {
1046 let s = t
1047 .format(&time::format_description::well_known::Rfc3339)
1048 .map_err(serde::ser::Error::custom)?;
1049
1050 s.serialize(serializer)
1051 } else {
1052 let n: Option<String> = None;
1053 n.serialize(serializer)
1054 }
1055}