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
872#[test]
877fn commit_status_relative_target_url_deserializes() {
878 let payload = r#"{
879 "context": "ci/build",
880 "created_at": null,
881 "creator": null,
882 "description": "",
883 "id": 1,
884 "status": "pending",
885 "target_url": "/owner/repo/actions/runs/187/jobs/0",
886 "updated_at": null,
887 "url": "https://example.com/api/v1/repos/owner/repo/statuses/abc"
888 }"#;
889 let cs: structs::CommitStatus = serde_json::from_str(payload).unwrap();
890 assert_eq!(
891 cs.target_url.as_deref(),
892 Some("/owner/repo/actions/runs/187/jobs/0")
893 );
894}
895
896#[test]
897fn missing_optional_fields() {
898 let raw_json = r#"
899 {}
900 "#;
901 serde_json::from_str::<structs::Repository>(raw_json).unwrap();
902 let raw_json = r#"
903 {
904 "avatar_url": "http://example.com"
905 }
906 "#;
907 serde_json::from_str::<structs::Repository>(raw_json).unwrap();
908}
909
910impl From<structs::DefaultMergeStyle> for structs::MergePullRequestOptionDo {
911 fn from(value: structs::DefaultMergeStyle) -> Self {
912 match value {
913 structs::DefaultMergeStyle::Merge => structs::MergePullRequestOptionDo::Merge,
914 structs::DefaultMergeStyle::Rebase => structs::MergePullRequestOptionDo::Rebase,
915 structs::DefaultMergeStyle::RebaseMerge => {
916 structs::MergePullRequestOptionDo::RebaseMerge
917 }
918 structs::DefaultMergeStyle::Squash => structs::MergePullRequestOptionDo::Squash,
919 structs::DefaultMergeStyle::FastForwardOnly => {
920 structs::MergePullRequestOptionDo::FastForwardOnly
921 }
922 }
923 }
924}
925
926mod sealed {
927 pub trait Sealed {}
928}
929
930pub trait Endpoint: sealed::Sealed {
931 type Response: FromResponse;
932 fn make_request(self) -> RawRequest;
933 fn handle_error(response: ApiResponse) -> Result<(ApiResponse, bool), ForgejoError>;
934}
935
936#[derive(Clone)]
937pub struct RawRequest {
938 method: reqwest::Method,
939 path: Cow<'static, str>,
940 query: Option<Vec<(&'static str, String)>>,
941 body: RequestBody,
942 page: Option<u32>,
943 limit: Option<u32>,
944}
945
946impl RawRequest {
947 pub(crate) fn wrap<E: Endpoint<Response = R>, R>(self, client: &Forgejo) -> Request<'_, E, R> {
948 Request {
949 inner: TypedRequest {
950 inner: self,
951 __endpoint: PhantomData,
952 __response: PhantomData,
953 },
954 client,
955 }
956 }
957
958 #[cfg(feature = "sync")]
959 pub(crate) fn wrap_sync<E: Endpoint<Response = R>, R>(
960 self,
961 client: &sync::Forgejo,
962 ) -> sync::Request<'_, E, R> {
963 sync::Request {
964 inner: TypedRequest {
965 inner: self,
966 __endpoint: PhantomData,
967 __response: PhantomData,
968 },
969 client,
970 }
971 }
972}
973
974pub trait FromResponse {
975 fn from_response(response: ApiResponse, has_body: bool) -> Result<Self, StructureError>
976 where
977 Self: Sized;
978}
979
980#[macro_export]
981macro_rules! impl_from_response {
982 ($t:ty) => {
983 impl $crate::FromResponse for $t {
984 $crate::json_impl!();
985 }
986 };
987}
988#[macro_export]
989#[doc(hidden)]
990macro_rules! json_impl {
991 () => {
992 fn from_response(
993 response: $crate::ApiResponse,
994 has_body: bool,
995 ) -> Result<Self, $crate::StructureError> {
996 soft_assert::soft_assert!(has_body, Err($crate::StructureError::EmptyResponse));
997 serde_json::from_slice(&response.body()).map_err(|e| $crate::StructureError::Serde {
998 e,
999 contents: response.body().clone(),
1000 })
1001 }
1002 };
1003}
1004
1005impl FromResponse for String {
1006 fn from_response(
1007 response: crate::ApiResponse,
1008 has_body: bool,
1009 ) -> Result<Self, crate::StructureError> {
1010 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
1011 Ok(std::str::from_utf8(&response.body)?.to_owned())
1012 }
1013}
1014
1015impl FromResponse for bytes::Bytes {
1016 fn from_response(
1017 response: crate::ApiResponse,
1018 has_body: bool,
1019 ) -> Result<Self, crate::StructureError> {
1020 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
1021 Ok(response.body.clone())
1022 }
1023}
1024
1025impl<T: FromResponse + serde::de::DeserializeOwned> FromResponse for Vec<T> {
1026 json_impl!();
1027}
1028
1029impl<K, V> FromResponse for BTreeMap<K, V>
1030where
1031 BTreeMap<K, V>: serde::de::DeserializeOwned,
1032{
1033 json_impl!();
1034}
1035
1036impl FromResponse for Vec<u8> {
1037 fn from_response(
1038 response: crate::ApiResponse,
1039 has_body: bool,
1040 ) -> Result<Self, crate::StructureError> {
1041 soft_assert::soft_assert!(has_body, Err(crate::StructureError::EmptyResponse));
1042 Ok(response.body.to_vec())
1043 }
1044}
1045
1046impl<
1047 T: FromResponse,
1048 H: for<'a> TryFrom<&'a reqwest::header::HeaderMap, Error = crate::StructureError>,
1049 > FromResponse for (H, T)
1050{
1051 fn from_response(
1052 response: crate::ApiResponse,
1053 has_body: bool,
1054 ) -> Result<Self, crate::StructureError> {
1055 let headers = H::try_from(&response.headers)?;
1056 let body = T::from_response(response, has_body)?;
1057 Ok((headers, body))
1058 }
1059}
1060
1061impl<T: FromResponse> FromResponse for Option<T> {
1062 fn from_response(
1063 response: crate::ApiResponse,
1064 has_body: bool,
1065 ) -> Result<Self, crate::StructureError> {
1066 if has_body {
1067 T::from_response(response, true).map(Some)
1068 } else {
1069 Ok(None)
1070 }
1071 }
1072}
1073
1074impl_from_response!(bool);
1075
1076impl FromResponse for () {
1077 fn from_response(_: crate::ApiResponse, _: bool) -> Result<Self, crate::StructureError> {
1078 Ok(())
1079 }
1080}
1081
1082#[derive(Clone)]
1083pub enum RequestBody {
1084 Json(bytes::Bytes),
1085 Form(Vec<(&'static str, Vec<u8>)>),
1086 None,
1087}
1088
1089pub struct TypedRequest<E, R> {
1090 inner: RawRequest,
1091 __endpoint: PhantomData<E>,
1092 __response: PhantomData<R>,
1093}
1094
1095impl<E: Endpoint, R: FromResponse> TypedRequest<E, R> {
1096 async fn send(&self, client: &Forgejo) -> Result<R, ForgejoError> {
1097 let (response, has_body) = E::handle_error(client.send_request(&self.inner).await?)?;
1098 Ok(R::from_response(response, has_body)?)
1099 }
1100
1101 #[cfg(feature = "sync")]
1102 fn send_sync(&self, client: &sync::Forgejo) -> Result<R, ForgejoError> {
1103 let (response, has_body) = E::handle_error(client.send_request(&self.inner)?)?;
1104 Ok(R::from_response(response, has_body)?)
1105 }
1106}
1107
1108pub struct ApiResponse {
1109 status_code: StatusCode,
1110 headers: reqwest::header::HeaderMap,
1111 body: bytes::Bytes,
1112}
1113
1114impl ApiResponse {
1115 pub fn status_code(&self) -> StatusCode {
1116 self.status_code
1117 }
1118
1119 pub fn headers(&self) -> &reqwest::header::HeaderMap {
1120 &self.headers
1121 }
1122
1123 pub fn body(&self) -> &bytes::Bytes {
1124 &self.body
1125 }
1126}
1127
1128pub struct Request<'a, E, R> {
1129 inner: TypedRequest<E, R>,
1130 client: &'a Forgejo,
1131}
1132
1133impl<'a, E: Endpoint, R: FromResponse> Request<'a, E, R> {
1134 pub async fn send(self) -> Result<R, ForgejoError> {
1135 self.inner.send(self.client).await
1136 }
1137
1138 pub fn response_type<T: FromResponse>(self) -> Request<'a, E, T> {
1139 Request {
1140 inner: TypedRequest {
1141 inner: self.inner.inner,
1142 __endpoint: PhantomData,
1143 __response: PhantomData,
1144 },
1145 client: self.client,
1146 }
1147 }
1148
1149 pub fn page(mut self, page: u32) -> Self {
1150 self.inner.inner.page = Some(page);
1151 self
1152 }
1153
1154 pub fn page_size(mut self, limit: u32) -> Self {
1155 self.inner.inner.limit = Some(limit);
1156 self
1157 }
1158}
1159
1160pub trait CountHeader: sealed::Sealed {
1161 fn count(&self) -> Option<usize>;
1162}
1163
1164pub trait PageSize: sealed::Sealed {
1165 fn page_size(&self) -> usize;
1166}
1167
1168impl<T> sealed::Sealed for Vec<T> {}
1169impl<T> PageSize for Vec<T> {
1170 fn page_size(&self) -> usize {
1171 self.len()
1172 }
1173}
1174
1175impl<'a, E: Endpoint, H: CountHeader, T: PageSize> Request<'a, E, (H, T)>
1176where
1177 (H, T): FromResponse,
1178{
1179 pub fn stream_pages(self) -> PageStream<'a, E, T, H> {
1180 PageStream {
1181 request: self,
1182 total_seen: 0,
1183 finished: false,
1184 fut: None,
1185 }
1186 }
1187}
1188
1189pub struct PageStream<'a, E: Endpoint, T, H> {
1190 request: Request<'a, E, (H, T)>,
1191 total_seen: usize,
1192 finished: bool,
1193 fut: Option<Pin<Box<dyn Future<Output = Result<(H, T), ForgejoError>> + Send + Sync + 'a>>>,
1194}
1195
1196impl<'a, E: Endpoint, T: PageSize, H: CountHeader> futures::stream::Stream
1197 for PageStream<'a, E, T, H>
1198where
1199 Self: Unpin + 'a,
1200 (H, T): FromResponse,
1201{
1202 type Item = Result<T, ForgejoError>;
1203
1204 fn poll_next(
1205 mut self: Pin<&mut Self>,
1206 cx: &mut std::task::Context<'_>,
1207 ) -> Poll<Option<Self::Item>> {
1208 if self.finished {
1209 return Poll::Ready(None);
1210 }
1211 match &mut self.fut {
1212 None => {
1213 let request = self.request.inner.inner.clone();
1214 let client = self.request.client;
1215 let fut = Box::pin(async move {
1216 E::handle_error(client.send_request(&request).await?).and_then(|(res, body)| {
1217 <(H, T)>::from_response(res, body).map_err(|e| e.into())
1218 })
1219 });
1220 self.fut = Some(fut);
1221 cx.waker().wake_by_ref();
1222 Poll::Pending
1223 }
1224 Some(fut) => {
1225 let (headers, page_content) = match fut.as_mut().poll(cx) {
1226 Poll::Ready(Ok(response)) => response,
1227 Poll::Ready(Err(e)) => {
1228 self.finished = true;
1229 return Poll::Ready(Some(Err(e)));
1230 }
1231 Poll::Pending => return Poll::Pending,
1232 };
1233 self.total_seen += page_content.page_size();
1234 let total_count = match headers.count() {
1235 Some(n) => n,
1236 None => {
1237 self.finished = true;
1238 return Poll::Ready(Some(Err(StructureError::HeaderMissing(
1239 "x-total-count",
1240 )
1241 .into())));
1242 }
1243 };
1244
1245 if self.total_seen >= total_count {
1246 self.finished = true;
1247 } else {
1248 self.request.inner.inner.page =
1249 Some(self.request.inner.inner.page.unwrap_or(1) + 1);
1250 self.fut = None;
1251 }
1252
1253 Poll::Ready(Some(Ok(page_content)))
1254 }
1255 }
1256 }
1257}
1258
1259impl<
1260 'a,
1261 E: Endpoint + Unpin + Send + Sync + 'a,
1262 T: Unpin + Send + Sync + 'a,
1263 H: CountHeader + Unpin + Send + Sync + 'a,
1264 > Request<'a, E, (H, Vec<T>)>
1265where
1266 (H, Vec<T>): FromResponse,
1267{
1268 pub fn stream(
1269 self,
1270 ) -> impl futures::Stream<Item = Result<T, ForgejoError>> + Send + Sync + use<'a, E, T, H> {
1271 use futures::TryStreamExt;
1272 self.stream_pages()
1273 .map_ok(|page| futures::stream::iter(page.into_iter().map(Ok)))
1274 .try_flatten()
1275 }
1276
1277 pub async fn all(self) -> Result<Vec<T>, ForgejoError> {
1278 use futures::TryStreamExt;
1279
1280 self.stream().try_collect().await
1281 }
1282}
1283
1284impl<'a, E: Endpoint, R: FromResponse> std::future::IntoFuture for Request<'a, E, R> {
1285 type Output = Result<R, ForgejoError>;
1286
1287 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'a>>;
1288
1289 fn into_future(self) -> Self::IntoFuture {
1290 Box::pin(async move {
1291 let (response, has_body) =
1292 E::handle_error(self.client.send_request(&self.inner.inner).await?)?;
1293 Ok(R::from_response(response, has_body)?)
1294 })
1295 }
1296}