1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::{
150 borrow::Cow, collections::BTreeMap, future::Future, marker::PhantomData, pin::Pin, task::Poll,
151};
152
153use reqwest::{Client, StatusCode};
154use serde::{Deserialize, Deserializer};
155use soft_assert::*;
156use url::Url;
157use zeroize::Zeroize;
158
159pub struct Forgejo {
163 url: Url,
164 client: Client,
165}
166
167mod generated;
168#[cfg(feature = "sync")]
169pub mod sync;
170
171#[derive(thiserror::Error, Debug)]
172pub enum ForgejoError {
173 #[error("url must have a host")]
174 HostRequired,
175 #[error("scheme must be http or https")]
176 HttpRequired,
177 #[error(transparent)]
178 ReqwestError(#[from] reqwest::Error),
179 #[error("API key should be ascii")]
180 KeyNotAscii,
181 #[error("the response from forgejo was not properly structured")]
182 BadStructure(#[from] StructureError),
183 #[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))]
184 UnexpectedStatusCode(StatusCode),
185 #[error(transparent)]
186 ApiError(#[from] ApiError),
187 #[error("the provided authorization was too long to accept")]
188 AuthTooLong,
189}
190
191#[derive(thiserror::Error, Debug)]
192pub enum StructureError {
193 #[error("{e}")]
194 Serde {
195 e: serde_json::Error,
196 contents: bytes::Bytes,
197 },
198 #[error(transparent)]
199 Utf8(#[from] std::str::Utf8Error),
200 #[error("failed to find header `{0}`")]
201 HeaderMissing(&'static str),
202 #[error("header was not ascii")]
203 HeaderNotAscii,
204 #[error("failed to parse header")]
205 HeaderParseFailed,
206 #[error("nothing was returned when a value was expected")]
207 EmptyResponse,
208}
209
210impl From<std::str::Utf8Error> for ForgejoError {
211 fn from(error: std::str::Utf8Error) -> Self {
212 Self::BadStructure(StructureError::Utf8(error))
213 }
214}
215
216#[derive(thiserror::Error, Debug)]
217pub struct ApiError {
218 pub message: Option<String>,
219 pub kind: ApiErrorKind,
220}
221
222impl ApiError {
223 fn new(message: Option<String>, kind: ApiErrorKind) -> Self {
224 Self { message, kind }
225 }
226
227 pub fn message(&self) -> Option<&str> {
228 self.message.as_deref()
229 }
230
231 pub fn error_kind(&self) -> &ApiErrorKind {
232 &self.kind
233 }
234}
235
236impl From<ApiErrorKind> for ApiError {
237 fn from(kind: ApiErrorKind) -> Self {
238 Self {
239 message: None,
240 kind,
241 }
242 }
243}
244
245impl std::fmt::Display for ApiError {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 match &self.message {
248 Some(message) => write!(f, "{}: {message}", self.kind),
249 None => write!(f, "{}", self.kind),
250 }
251 }
252}
253
254#[derive(thiserror::Error, Debug)]
255pub enum ApiErrorKind {
256 #[error("api error")]
257 Generic,
258 #[error("access denied")]
259 Forbidden,
260 #[error("invalid topics")]
261 InvalidTopics { invalid_topics: Option<Vec<String>> },
262 #[error("not found")]
263 NotFound { errors: Option<Vec<String>> },
264 #[error("repo archived")]
265 RepoArchived,
266 #[error("unauthorized")]
267 Unauthorized,
268 #[error("validation failed")]
269 ValidationFailed,
270 #[error("status code {0}")]
271 Other(reqwest::StatusCode),
272}
273
274impl From<structs::APIError> for ApiError {
275 fn from(value: structs::APIError) -> Self {
276 Self::new(value.message, ApiErrorKind::Generic)
277 }
278}
279impl From<structs::APIForbiddenError> for ApiError {
280 fn from(value: structs::APIForbiddenError) -> Self {
281 Self::new(value.message, ApiErrorKind::Forbidden)
282 }
283}
284impl From<structs::APIInvalidTopicsError> for ApiError {
285 fn from(value: structs::APIInvalidTopicsError) -> Self {
286 Self::new(
287 value.message,
288 ApiErrorKind::InvalidTopics {
289 invalid_topics: value.invalid_topics,
290 },
291 )
292 }
293}
294impl From<structs::APINotFound> for ApiError {
295 fn from(value: structs::APINotFound) -> Self {
296 Self::new(
297 value.message,
298 ApiErrorKind::NotFound {
299 errors: value.errors,
300 },
301 )
302 }
303}
304impl From<structs::APIRepoArchivedError> for ApiError {
305 fn from(value: structs::APIRepoArchivedError) -> Self {
306 Self::new(value.message, ApiErrorKind::RepoArchived)
307 }
308}
309impl From<structs::APIUnauthorizedError> for ApiError {
310 fn from(value: structs::APIUnauthorizedError) -> Self {
311 Self::new(value.message, ApiErrorKind::Unauthorized)
312 }
313}
314impl From<structs::APIValidationError> for ApiError {
315 fn from(value: structs::APIValidationError) -> Self {
316 Self::new(value.message, ApiErrorKind::ValidationFailed)
317 }
318}
319impl From<reqwest::StatusCode> for ApiError {
320 fn from(value: reqwest::StatusCode) -> Self {
321 match value {
322 reqwest::StatusCode::NOT_FOUND => ApiErrorKind::NotFound { errors: None },
323 reqwest::StatusCode::FORBIDDEN => ApiErrorKind::Forbidden,
324 reqwest::StatusCode::UNAUTHORIZED => ApiErrorKind::Unauthorized,
325 _ => ApiErrorKind::Other(value),
326 }
327 .into()
328 }
329}
330impl From<OAuthError> for ApiError {
331 fn from(value: OAuthError) -> Self {
332 Self::new(Some(value.error_description), ApiErrorKind::Generic)
333 }
334}
335
336pub enum Auth<'a> {
338 Token(&'a str),
347 OAuth2(&'a str),
353 Password {
356 username: &'a str,
357 password: &'a str,
358 mfa: Option<&'a str>,
359 },
360 None,
362}
363
364impl Auth<'_> {
365 fn to_headers(&self) -> Result<reqwest::header::HeaderMap, ForgejoError> {
366 let mut headers = reqwest::header::HeaderMap::new();
367 match self {
368 Auth::Token(token) => {
369 let mut header: reqwest::header::HeaderValue = format!("token {token}")
370 .try_into()
371 .map_err(|_| ForgejoError::KeyNotAscii)?;
372 header.set_sensitive(true);
373 headers.insert("Authorization", header);
374 }
375 Auth::Password {
376 username,
377 password,
378 mfa,
379 } => {
380 let unencoded_len = username.len() + password.len() + 1;
381 let unpadded_len = unencoded_len
382 .checked_mul(4)
383 .ok_or(ForgejoError::AuthTooLong)?
384 .div_ceil(3);
385 let len = unpadded_len.div_ceil(4) * 4;
387 let mut bytes = vec![0; len];
388
389 let mut encoder = base64ct::Encoder::<base64ct::Base64>::new(&mut bytes).unwrap();
391
392 encoder.encode(username.as_bytes()).unwrap();
394 encoder.encode(b":").unwrap();
395 encoder.encode(password.as_bytes()).unwrap();
396
397 let b64 = encoder.finish().unwrap();
398
399 let mut header: reqwest::header::HeaderValue =
400 format!("Basic {b64}").try_into().unwrap(); header.set_sensitive(true);
402 headers.insert("Authorization", header);
403
404 bytes.zeroize();
405
406 if let Some(mfa) = mfa {
407 let mut key_header: reqwest::header::HeaderValue =
408 (*mfa).try_into().map_err(|_| ForgejoError::KeyNotAscii)?;
409 key_header.set_sensitive(true);
410 headers.insert("X-FORGEJO-OTP", key_header);
411 }
412 }
413 Auth::OAuth2(token) => {
414 let mut header: reqwest::header::HeaderValue = format!("Bearer {token}")
415 .try_into()
416 .map_err(|_| ForgejoError::KeyNotAscii)?;
417 header.set_sensitive(true);
418 headers.insert("Authorization", header);
419 }
420 Auth::None => (),
421 }
422 Ok(headers)
423 }
424}
425
426#[test]
427fn to_headers_token() {
428 let token = Auth::Token("hello");
429
430 let headers = token.to_headers().unwrap();
431
432 assert!(headers["Authorization"].is_sensitive());
433 assert_eq!(headers["Authorization"].to_str().unwrap(), "token hello");
434}
435
436#[test]
437fn to_headers_token_error() {
438 let token = Auth::Token("🎉");
439
440 let headers = token.to_headers().unwrap();
441 let expected_error = headers["Authorization"].to_str().unwrap_err();
442
443 assert_eq!(
444 expected_error.to_string(),
445 "failed to convert header to a str"
446 );
447}
448
449#[test]
450fn to_headers_password() {
451 let auth_password = Auth::Password {
452 username: "username",
453 password: "password",
454 mfa: None,
455 };
456
457 let headers = auth_password.to_headers().unwrap();
458
459 assert!(headers["Authorization"].is_sensitive());
460 assert_eq!(
461 headers["Authorization"].to_str().unwrap(),
462 "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
463 );
464 assert!(headers.get("X-FORGEJO-OTP").is_none());
465}
466
467#[test]
468fn to_headers_password_with_mfa() {
469 let mfa_code: Option<&str> = Some("123456");
470 let auth_password_with_mfa = Auth::Password {
471 username: "username",
472 password: "password",
473 mfa: mfa_code,
474 };
475
476 let headers = auth_password_with_mfa.to_headers().unwrap();
477
478 assert!(headers["Authorization"].is_sensitive());
479 assert_eq!(
480 headers["Authorization"].to_str().unwrap(),
481 "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
482 );
483 assert_eq!(headers["X-FORGEJO-OTP"].to_str().unwrap(), "123456");
484}
485
486#[test]
487fn to_headers_password_with_mfa_error() {
488 let mfa_code: Option<&str> = Some("🎉");
489 let auth_password_with_mfa = Auth::Password {
490 username: "username",
491 password: "password",
492 mfa: mfa_code,
493 };
494 let headers = auth_password_with_mfa.to_headers().unwrap();
495 let expected_error = headers["X-FORGEJO-OTP"].to_str().unwrap_err();
496
497 assert_eq!(
498 expected_error.to_string(),
499 "failed to convert header to a str"
500 );
501}
502
503#[test]
504fn to_headers_oauth2() {
505 let oauth_token = Auth::OAuth2("some token");
506
507 let headers = oauth_token.to_headers().unwrap();
508
509 assert!(headers["Authorization"].is_sensitive());
510 assert_eq!(
511 headers["Authorization"].to_str().unwrap(),
512 "Bearer some token"
513 );
514}
515
516#[test]
517fn to_headers_oauth2_error() {
518 let oauth_token = Auth::OAuth2("🎉");
519
520 let headers = oauth_token.to_headers().unwrap();
521 let expected_error = headers["Authorization"].to_str().unwrap_err();
522
523 assert_eq!(
524 expected_error.to_string(),
525 "failed to convert header to a str"
526 );
527}
528
529#[test]
530fn to_headers_none() {
531 let no_auth = Auth::None;
532
533 let headers = no_auth.to_headers().unwrap();
534
535 assert!(headers.is_empty());
536}
537
538impl Forgejo {
539 pub fn new(auth: Auth, url: Url) -> Result<Self, ForgejoError> {
544 Self::with_user_agent(auth, url, "forgejo-api-rs")
545 }
546
547 pub fn with_user_agent(auth: Auth, url: Url, user_agent: &str) -> Result<Self, ForgejoError> {
550 soft_assert!(
551 matches!(url.scheme(), "http" | "https"),
552 Err(ForgejoError::HttpRequired)
553 );
554
555 let client = Client::builder()
556 .user_agent(user_agent)
557 .default_headers(auth.to_headers()?)
558 .build()?;
559 Ok(Self { url, client })
560 }
561
562 pub async fn download_release_attachment(
563 &self,
564 owner: &str,
565 repo: &str,
566 release: i64,
567 attach: i64,
568 ) -> Result<bytes::Bytes, ForgejoError> {
569 let release = self
570 .repo_get_release_attachment(owner, repo, release, attach)
571 .await?;
572 let mut url = self.url.clone();
573 url.path_segments_mut()
574 .unwrap()
575 .pop_if_empty()
576 .extend(["attachments", &release.uuid.unwrap().to_string()]);
577 let request = self.client.get(url).build()?;
578 Ok(self.client.execute(request).await?.bytes().await?)
579 }
580
581 pub async fn oauth_get_access_token(
585 &self,
586 body: structs::OAuthTokenRequest<'_>,
587 ) -> Result<structs::OAuthToken, ForgejoError> {
588 let url = self.url.join("login/oauth/access_token").unwrap();
589 let request = self.client.post(url).json(&body).build()?;
590 let response = self.client.execute(request).await?;
591 match response.status() {
592 reqwest::StatusCode::OK => Ok(response.json().await?),
593 status if status.is_client_error() => {
594 let err = response.json::<OAuthError>().await?;
595 Err(ApiError::from(err).into())
596 }
597 _ => Err(ForgejoError::UnexpectedStatusCode(response.status())),
598 }
599 }
600
601 pub async fn send_request(&self, request: &RawRequest) -> Result<ApiResponse, ForgejoError> {
602 let mut url = self
603 .url
604 .join(&request.path)
605 .expect("url fail. bug in forgejo-api");
606
607 {
611 let mut query_pairs = url.query_pairs_mut();
612 if let Some(query) = &request.query {
613 query_pairs.extend_pairs(query.iter());
614 }
615 if let Some(page) = request.page {
616 query_pairs.append_pair("page", &format!("{page}"));
617 }
618 if let Some(limit) = request.limit {
619 query_pairs.append_pair("limit", &format!("{limit}"));
620 }
621 }
622
623 let mut reqwest_request = self.client.request(request.method.clone(), url);
624 reqwest_request = match &request.body {
625 RequestBody::Json(bytes) => reqwest_request
626 .body(bytes.clone())
627 .header(reqwest::header::CONTENT_TYPE, "application/json"),
628 RequestBody::Form(list) => {
629 let mut form = reqwest::multipart::Form::new();
630 for (k, v) in list {
631 form = form.part(
632 *k,
633 reqwest::multipart::Part::bytes(v.clone()).file_name("file"),
634 );
635 }
636 reqwest_request.multipart(form)
637 }
638 RequestBody::None => reqwest_request,
639 };
640 let mut reqwest_response = reqwest_request.send().await?;
641 let response = ApiResponse {
642 status_code: reqwest_response.status(),
643 headers: std::mem::take(reqwest_response.headers_mut()),
644 body: reqwest_response.bytes().await?,
645 };
646 Ok(response)
647 }
648
649 pub async fn hit_endpoint<E: Endpoint, R: FromResponse>(
650 &self,
651 endpoint: E,
652 ) -> Result<R, ForgejoError> {
653 let (response, has_body) =
654 E::handle_error(self.send_request(&endpoint.make_request()).await?)?;
655 Ok(R::from_response(response, has_body)?)
656 }
657}
658
659#[derive(serde::Deserialize)]
660struct OAuthError {
661 error_description: String,
662 }
665
666pub mod structs {
667 pub use crate::generated::structs::*;
668
669 #[derive(serde::Serialize)]
673 #[serde(tag = "grant_type")]
674 pub enum OAuthTokenRequest<'a> {
675 #[serde(rename = "authorization_code")]
680 Confidential {
681 client_id: &'a str,
682 client_secret: &'a str,
683 code: &'a str,
684 redirect_uri: url::Url,
685 },
686 #[serde(rename = "authorization_code")]
691 Public {
692 client_id: &'a str,
693 code_verifier: &'a str,
694 code: &'a str,
695 redirect_uri: url::Url,
696 },
697 #[serde(rename = "refresh_token")]
699 Refresh {
700 refresh_token: &'a str,
701 client_id: &'a str,
702 client_secret: &'a str,
703 },
704 }
705
706 #[derive(serde::Deserialize)]
707 pub struct OAuthToken {
708 pub access_token: String,
709 pub refresh_token: String,
710 pub token_type: String,
711 pub expires_in: u32,
713 }
714}
715
716fn none_if_blank_url<'de, D: serde::Deserializer<'de>>(
719 deserializer: D,
720) -> Result<Option<Url>, D::Error> {
721 use serde::de::{Error, Unexpected, Visitor};
722 use std::fmt;
723
724 struct EmptyUrlVisitor;
725
726 impl<'de> Visitor<'de> for EmptyUrlVisitor {
727 type Value = Option<Url>;
728
729 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
730 formatter.write_str("option")
731 }
732
733 #[inline]
734 fn visit_unit<E>(self) -> Result<Self::Value, E>
735 where
736 E: Error,
737 {
738 Ok(None)
739 }
740
741 #[inline]
742 fn visit_none<E>(self) -> Result<Self::Value, E>
743 where
744 E: Error,
745 {
746 Ok(None)
747 }
748
749 #[inline]
750 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
751 where
752 D: serde::Deserializer<'de>,
753 {
754 let s: String = serde::Deserialize::deserialize(deserializer)?;
755 if s.is_empty() {
756 return Ok(None);
757 }
758 Url::parse(&s)
759 .map_err(|err| {
760 let err_s = format!("{}", err);
761 Error::invalid_value(Unexpected::Str(&s), &err_s.as_str())
762 })
763 .map(Some)
764 }
765
766 #[inline]
767 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
768 where
769 E: Error,
770 {
771 if s.is_empty() {
772 return Ok(None);
773 }
774 Url::parse(s)
775 .map_err(|err| {
776 let err_s = format!("{err}");
777 Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
778 })
779 .map(Some)
780 }
781 }
782
783 deserializer.deserialize_option(EmptyUrlVisitor)
784}
785
786#[allow(dead_code)] fn deserialize_ssh_url<'de, D, DE>(deserializer: D) -> Result<Url, DE>
788where
789 D: Deserializer<'de>,
790 DE: serde::de::Error,
791{
792 let raw_url: String = String::deserialize(deserializer).map_err(DE::custom)?;
793 parse_ssh_url(&raw_url).map_err(DE::custom)
794}
795
796fn deserialize_optional_ssh_url<'de, D, DE>(deserializer: D) -> Result<Option<Url>, DE>
797where
798 D: Deserializer<'de>,
799 DE: serde::de::Error,
800{
801 let raw_url: Option<String> = Option::deserialize(deserializer).map_err(DE::custom)?;
802 raw_url
803 .as_ref()
804 .map(parse_ssh_url)
805 .map(|res| res.map_err(DE::custom))
806 .transpose()
807 .or(Ok(None))
808}
809
810fn requested_reviewers_ignore_null<'de, D, DE>(
811 deserializer: D,
812) -> Result<Option<Vec<structs::User>>, DE>
813where
814 D: Deserializer<'de>,
815 DE: serde::de::Error,
816{
817 let list: Option<Vec<Option<structs::User>>> =
818 Option::deserialize(deserializer).map_err(DE::custom)?;
819 Ok(list.map(|list| list.into_iter().flatten().collect::<Vec<_>>()))
820}
821
822fn parse_ssh_url(raw_url: &String) -> Result<Url, url::ParseError> {
823 Url::parse(raw_url).or_else(|_| {
826 let url = format!("ssh://{url}", url = raw_url.replace(":", "/"));
829 Url::parse(url.as_str())
830 })
831}
832
833#[test]
834fn ssh_url_deserialization() {
835 #[derive(serde::Deserialize)]
836 struct SshUrl {
837 #[serde(deserialize_with = "deserialize_ssh_url")]
838 url: url::Url,
839 }
840 let full_url = r#"{ "url": "ssh://git@codeberg.org/Cyborus/forgejo-api" }"#;
841 let ssh_url = r#"{ "url": "git@codeberg.org:Cyborus/forgejo-api" }"#;
842
843 let full_url_de =
844 serde_json::from_str::<SshUrl>(full_url).expect("failed to deserialize full url");
845 let ssh_url_de =
846 serde_json::from_str::<SshUrl>(ssh_url).expect("failed to deserialize ssh url");
847
848 let expected = "ssh://git@codeberg.org/Cyborus/forgejo-api";
849 assert_eq!(full_url_de.url.as_str(), expected);
850 assert_eq!(ssh_url_de.url.as_str(), expected);
851
852 #[derive(serde::Deserialize)]
853 struct OptSshUrl {
854 #[serde(deserialize_with = "deserialize_optional_ssh_url")]
855 url: Option<url::Url>,
856 }
857 let null_url = r#"{ "url": null }"#;
858
859 let full_url_de = serde_json::from_str::<OptSshUrl>(full_url)
860 .expect("failed to deserialize optional full url");
861 let ssh_url_de =
862 serde_json::from_str::<OptSshUrl>(ssh_url).expect("failed to deserialize optional ssh url");
863 let null_url_de =
864 serde_json::from_str::<OptSshUrl>(null_url).expect("failed to deserialize null url");
865
866 let expected = Some("ssh://git@codeberg.org/Cyborus/forgejo-api");
867 assert_eq!(full_url_de.url.as_ref().map(|u| u.as_ref()), expected);
868 assert_eq!(ssh_url_de.url.as_ref().map(|u| u.as_ref()), expected);
869 assert!(null_url_de.url.is_none());
870}
871
872impl From<structs::DefaultMergeStyle> for structs::MergePullRequestOptionDo {
873 fn from(value: structs::DefaultMergeStyle) -> Self {
874 match value {
875 structs::DefaultMergeStyle::Merge => structs::MergePullRequestOptionDo::Merge,
876 structs::DefaultMergeStyle::Rebase => structs::MergePullRequestOptionDo::Rebase,
877 structs::DefaultMergeStyle::RebaseMerge => {
878 structs::MergePullRequestOptionDo::RebaseMerge
879 }
880 structs::DefaultMergeStyle::Squash => structs::MergePullRequestOptionDo::Squash,
881 structs::DefaultMergeStyle::FastForwardOnly => {
882 structs::MergePullRequestOptionDo::FastForwardOnly
883 }
884 }
885 }
886}
887
888mod sealed {
889 pub trait Sealed {}
890}
891
892pub trait Endpoint: sealed::Sealed {
893 type Response: FromResponse;
894 fn make_request(self) -> RawRequest;
895 fn handle_error(response: ApiResponse) -> Result<(ApiResponse, bool), ForgejoError>;
896}
897
898#[derive(Clone)]
899pub struct RawRequest {
900 method: reqwest::Method,
901 path: Cow<'static, str>,
902 query: Option<Vec<(&'static str, String)>>,
903 body: RequestBody,
904 page: Option<u32>,
905 limit: Option<u32>,
906}
907
908impl RawRequest {
909 pub(crate) fn wrap<E: Endpoint<Response = R>, R>(self, client: &Forgejo) -> Request<'_, E, R> {
910 Request {
911 inner: TypedRequest {
912 inner: self,
913 __endpoint: PhantomData,
914 __response: PhantomData,
915 },
916 client,
917 }
918 }
919
920 #[cfg(feature = "sync")]
921 pub(crate) fn wrap_sync<E: Endpoint<Response = R>, R>(
922 self,
923 client: &sync::Forgejo,
924 ) -> sync::Request<'_, E, R> {
925 sync::Request {
926 inner: TypedRequest {
927 inner: self,
928 __endpoint: PhantomData,
929 __response: PhantomData,
930 },
931 client,
932 }
933 }
934}
935
936pub trait FromResponse {
937 fn from_response(response: ApiResponse, has_body: bool) -> Result<Self, StructureError>
938 where
939 Self: Sized;
940}
941
942#[macro_export]
943macro_rules! impl_from_response {
944 ($t:ty) => {
945 impl $crate::FromResponse for $t {
946 $crate::json_impl!();
947 }
948 };
949}
950#[macro_export]
951#[doc(hidden)]
952macro_rules! json_impl {
953 () => {
954 fn from_response(
955 response: $crate::ApiResponse,
956 has_body: bool,
957 ) -> Result<Self, $crate::StructureError> {
958 soft_assert::soft_assert!(has_body, Err($crate::StructureError::EmptyResponse));
959 serde_json::from_slice(&response.body()).map_err(|e| $crate::StructureError::Serde {
960 e,
961 contents: response.body().clone(),
962 })
963 }
964 };
965}
966
967impl FromResponse for String {
968 fn from_response(
969 response: crate::ApiResponse,
970 has_body: bool,
971 ) -> Result<Self, crate::StructureError> {
972 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
973 Ok(std::str::from_utf8(&response.body)?.to_owned())
974 }
975}
976
977impl FromResponse for bytes::Bytes {
978 fn from_response(
979 response: crate::ApiResponse,
980 has_body: bool,
981 ) -> Result<Self, crate::StructureError> {
982 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
983 Ok(response.body.clone())
984 }
985}
986
987impl<T: FromResponse + serde::de::DeserializeOwned> FromResponse for Vec<T> {
988 json_impl!();
989}
990
991impl<K, V> FromResponse for BTreeMap<K, V>
992where
993 BTreeMap<K, V>: serde::de::DeserializeOwned,
994{
995 json_impl!();
996}
997
998impl FromResponse for Vec<u8> {
999 fn from_response(
1000 response: crate::ApiResponse,
1001 has_body: bool,
1002 ) -> Result<Self, crate::StructureError> {
1003 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
1004 Ok(response.body.to_vec())
1005 }
1006}
1007
1008impl<
1009 T: FromResponse,
1010 H: for<'a> TryFrom<&'a reqwest::header::HeaderMap, Error = crate::StructureError>,
1011 > FromResponse for (H, T)
1012{
1013 fn from_response(
1014 response: crate::ApiResponse,
1015 has_body: bool,
1016 ) -> Result<Self, crate::StructureError> {
1017 let headers = H::try_from(&response.headers)?;
1018 let body = T::from_response(response, has_body)?;
1019 Ok((headers, body))
1020 }
1021}
1022
1023impl<T: FromResponse> FromResponse for Option<T> {
1024 fn from_response(
1025 response: crate::ApiResponse,
1026 has_body: bool,
1027 ) -> Result<Self, crate::StructureError> {
1028 if has_body {
1029 T::from_response(response, true).map(Some)
1030 } else {
1031 Ok(None)
1032 }
1033 }
1034}
1035
1036impl_from_response!(bool);
1037
1038impl FromResponse for () {
1039 fn from_response(_: crate::ApiResponse, _: bool) -> Result<Self, crate::StructureError> {
1040 Ok(())
1041 }
1042}
1043
1044#[derive(Clone)]
1045pub enum RequestBody {
1046 Json(bytes::Bytes),
1047 Form(Vec<(&'static str, Vec<u8>)>),
1048 None,
1049}
1050
1051pub struct TypedRequest<E, R> {
1052 inner: RawRequest,
1053 __endpoint: PhantomData<E>,
1054 __response: PhantomData<R>,
1055}
1056
1057impl<E: Endpoint, R: FromResponse> TypedRequest<E, R> {
1058 async fn send(&self, client: &Forgejo) -> Result<R, ForgejoError> {
1059 let (response, has_body) = E::handle_error(client.send_request(&self.inner).await?)?;
1060 Ok(R::from_response(response, has_body)?)
1061 }
1062
1063 #[cfg(feature = "sync")]
1064 fn send_sync(&self, client: &sync::Forgejo) -> Result<R, ForgejoError> {
1065 let (response, has_body) = E::handle_error(client.send_request(&self.inner)?)?;
1066 Ok(R::from_response(response, has_body)?)
1067 }
1068}
1069
1070pub struct ApiResponse {
1071 status_code: StatusCode,
1072 headers: reqwest::header::HeaderMap,
1073 body: bytes::Bytes,
1074}
1075
1076impl ApiResponse {
1077 pub fn status_code(&self) -> StatusCode {
1078 self.status_code
1079 }
1080
1081 pub fn headers(&self) -> &reqwest::header::HeaderMap {
1082 &self.headers
1083 }
1084
1085 pub fn body(&self) -> &bytes::Bytes {
1086 &self.body
1087 }
1088}
1089
1090pub struct Request<'a, E, R> {
1091 inner: TypedRequest<E, R>,
1092 client: &'a Forgejo,
1093}
1094
1095impl<'a, E: Endpoint, R: FromResponse> Request<'a, E, R> {
1096 pub async fn send(self) -> Result<R, ForgejoError> {
1097 self.inner.send(self.client).await
1098 }
1099
1100 pub fn response_type<T: FromResponse>(self) -> Request<'a, E, T> {
1101 Request {
1102 inner: TypedRequest {
1103 inner: self.inner.inner,
1104 __endpoint: PhantomData,
1105 __response: PhantomData,
1106 },
1107 client: self.client,
1108 }
1109 }
1110
1111 pub fn page(mut self, page: u32) -> Self {
1112 self.inner.inner.page = Some(page);
1113 self
1114 }
1115
1116 pub fn page_size(mut self, limit: u32) -> Self {
1117 self.inner.inner.limit = Some(limit);
1118 self
1119 }
1120}
1121
1122pub trait CountHeader: sealed::Sealed {
1123 fn count(&self) -> Option<usize>;
1124}
1125
1126pub trait PageSize: sealed::Sealed {
1127 fn page_size(&self) -> usize;
1128}
1129
1130impl<T> sealed::Sealed for Vec<T> {}
1131impl<T> PageSize for Vec<T> {
1132 fn page_size(&self) -> usize {
1133 self.len()
1134 }
1135}
1136
1137impl<'a, E: Endpoint, H: CountHeader, T: PageSize> Request<'a, E, (H, T)>
1138where
1139 (H, T): FromResponse,
1140{
1141 pub fn stream_pages(self) -> PageStream<'a, E, T, H> {
1142 PageStream {
1143 request: self,
1144 total_seen: 0,
1145 finished: false,
1146 fut: None,
1147 }
1148 }
1149}
1150
1151pub struct PageStream<'a, E: Endpoint, T, H> {
1152 request: Request<'a, E, (H, T)>,
1153 total_seen: usize,
1154 finished: bool,
1155 fut: Option<Pin<Box<dyn Future<Output = Result<(H, T), ForgejoError>> + Send + Sync + 'a>>>,
1156}
1157
1158impl<'a, E: Endpoint, T: PageSize, H: CountHeader> futures::stream::Stream
1159 for PageStream<'a, E, T, H>
1160where
1161 Self: Unpin + 'a,
1162 (H, T): FromResponse,
1163{
1164 type Item = Result<T, ForgejoError>;
1165
1166 fn poll_next(
1167 mut self: Pin<&mut Self>,
1168 cx: &mut std::task::Context<'_>,
1169 ) -> Poll<Option<Self::Item>> {
1170 if self.finished {
1171 return Poll::Ready(None);
1172 }
1173 match &mut self.fut {
1174 None => {
1175 let request = self.request.inner.inner.clone();
1176 let client = self.request.client;
1177 let fut = Box::pin(async move {
1178 E::handle_error(client.send_request(&request).await?).and_then(|(res, body)| {
1179 <(H, T)>::from_response(res, body).map_err(|e| e.into())
1180 })
1181 });
1182 self.fut = Some(fut);
1183 cx.waker().wake_by_ref();
1184 Poll::Pending
1185 }
1186 Some(fut) => {
1187 let (headers, page_content) = match fut.as_mut().poll(cx) {
1188 Poll::Ready(Ok(response)) => response,
1189 Poll::Ready(Err(e)) => {
1190 self.finished = true;
1191 return Poll::Ready(Some(Err(e)));
1192 }
1193 Poll::Pending => return Poll::Pending,
1194 };
1195 self.total_seen += page_content.page_size();
1196 let total_count = match headers.count() {
1197 Some(n) => n,
1198 None => {
1199 self.finished = true;
1200 return Poll::Ready(Some(Err(StructureError::HeaderMissing(
1201 "x-total-count",
1202 )
1203 .into())));
1204 }
1205 };
1206
1207 if self.total_seen >= total_count {
1208 self.finished = true;
1209 } else {
1210 self.request.inner.inner.page =
1211 Some(self.request.inner.inner.page.unwrap_or(1) + 1);
1212 self.fut = None;
1213 }
1214
1215 Poll::Ready(Some(Ok(page_content)))
1216 }
1217 }
1218 }
1219}
1220
1221impl<
1222 'a,
1223 E: Endpoint + Unpin + Send + Sync + 'a,
1224 T: Unpin + Send + Sync + 'a,
1225 H: CountHeader + Unpin + Send + Sync + 'a,
1226 > Request<'a, E, (H, Vec<T>)>
1227where
1228 (H, Vec<T>): FromResponse,
1229{
1230 pub fn stream(
1231 self,
1232 ) -> impl futures::Stream<Item = Result<T, ForgejoError>> + Send + Sync + use<'a, E, T, H> {
1233 use futures::TryStreamExt;
1234 self.stream_pages()
1235 .map_ok(|page| futures::stream::iter(page.into_iter().map(Ok)))
1236 .try_flatten()
1237 }
1238
1239 pub async fn all(self) -> Result<Vec<T>, ForgejoError> {
1240 use futures::TryStreamExt;
1241
1242 self.stream().try_collect().await
1243 }
1244}
1245
1246impl<'a, E: Endpoint, R: FromResponse> std::future::IntoFuture for Request<'a, E, R> {
1247 type Output = Result<R, ForgejoError>;
1248
1249 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'a>>;
1250
1251 fn into_future(self) -> Self::IntoFuture {
1252 Box::pin(async move {
1253 let (response, has_body) =
1254 E::handle_error(self.client.send_request(&self.inner.inner).await?)?;
1255 Ok(R::from_response(response, has_body)?)
1256 })
1257 }
1258}