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