octocrate_core/
request.rs

1#[cfg(feature = "file-body")]
2use crate::utils::file_to_body;
3use crate::{
4  api_config::SharedAPIConfig,
5  error::{APIErrorResponse, Error},
6  request_builder::RequestBuilder,
7  response::GitHubResponse,
8};
9#[cfg(feature = "multipart")]
10use reqwest::multipart::Form;
11use std::marker::PhantomData;
12#[cfg(feature = "file-body")]
13use tokio::fs::File;
14
15pub struct Request<Body, Query, Response> {
16  pub(crate) builder: reqwest::RequestBuilder,
17  pub(crate) api_config: SharedAPIConfig,
18  pub(crate) _body: PhantomData<Body>,
19  pub(crate) _query: PhantomData<Query>,
20  pub(crate) _response: PhantomData<Response>,
21}
22
23impl<Body, Query, ResponseData> Request<Body, Query, ResponseData>
24where
25  Body: serde::Serialize,
26  Query: serde::Serialize,
27  ResponseData: serde::de::DeserializeOwned,
28{
29  pub fn builder(config: &SharedAPIConfig) -> RequestBuilder<Body, Query, ResponseData> {
30    RequestBuilder::new(config)
31  }
32
33  pub fn query(mut self, query: &Query) -> Self {
34    self.builder = self.builder.query(query);
35
36    self
37  }
38
39  #[cfg(feature = "multipart")]
40  pub fn multipart(mut self, form: Form) -> Self {
41    self.builder = self.builder.multipart(form);
42
43    self
44  }
45
46  pub fn header<K, V>(mut self, key: K, value: V) -> Self
47  where
48    K: Into<String>,
49    V: Into<String>,
50  {
51    let key = key.into();
52    let value = value.into();
53    self.builder = self.builder.header(key, value);
54
55    self
56  }
57
58  #[cfg(feature = "file-body")]
59  pub fn file(mut self, file: File) -> Self {
60    self.builder = self.builder.body(file_to_body(file));
61
62    self
63  }
64
65  pub fn body(mut self, body: &Body) -> Self {
66    self.builder = self.builder.json(body);
67
68    self
69  }
70
71  /// Send the request and wrap the response with a GitHubResponse struct which
72  /// proved access to some of the response metadata.
73  pub async fn send_with_response(self) -> Result<GitHubResponse<ResponseData>, Error> {
74    let mut builder = self
75      .builder
76      .header("User-Agent", "octocrate")
77      .header("Accept", "application/vnd.github+json");
78
79    if let Some(token) = &self.api_config.token {
80      if let Some(token) = token.get_token() {
81        builder = builder.header("Authorization", format!("Bearer {}", token));
82      }
83    }
84
85    let res = builder.send().await;
86    match res {
87      Ok(res) => {
88        let status = res.status();
89        let content_length = res.content_length();
90        let headers = res.headers().clone();
91        let version = res.version();
92        let url = res.url().clone();
93
94        if !status.is_success() {
95          if let Ok(error_response) = res.json::<APIErrorResponse>().await {
96            return Err(Error::RequestFailed(error_response));
97          }
98
99          let err_msg = format!("Request failed with {}", status);
100          return Err(Error::Error(err_msg));
101        }
102
103        let res = res.text().await.map_err(|err| {
104          Error::Error(format!(
105            "Failed to read response with status {}: {}",
106            status, err
107          ))
108        })?;
109
110        match serde_json::from_str(&res) {
111          Ok(data) => {
112            let github_response = GitHubResponse {
113              content_length,
114              data,
115              headers,
116              status,
117              url,
118              version,
119            };
120            Ok(github_response)
121          }
122          Err(error) => Err(Error::Error(format!(
123            r#"Failed to parse response with status {}: {}
124
125              Response: {}"#,
126            status, error, res
127          ))),
128        }
129      }
130      Err(err) => Err(Error::Error(err.to_string())),
131    }
132  }
133
134  pub async fn send(self) -> Result<ResponseData, Error> {
135    Ok(self.send_with_response().await?.data)
136  }
137}
138
139#[cfg(feature = "pagination")]
140impl<Body, Query, ResponseData> Request<Body, Query, ResponseData>
141where
142  Body: serde::Serialize,
143  Query: serde::Serialize,
144  ResponseData: serde::de::DeserializeOwned + IntoIterator,
145{
146  pub async fn paginated_send(self) -> Result<octocrate_types::PaginatedData<ResponseData>, Error> {
147    Ok(self.paginated_send_with_response().await?.paginate())
148  }
149
150  pub async fn paginated_send_with_response(
151    self,
152  ) -> Result<crate::GitHubPaginatedResponse<ResponseData>, Error> {
153    Ok(self.send_with_response().await?.into())
154  }
155}
156
157#[cfg(test)]
158mod tests {
159  use super::*;
160  use crate::api_config::APIConfig;
161
162  #[tokio::test]
163  async fn test_request_builder() {
164    let config = APIConfig::default().shared();
165
166    #[derive(serde::Serialize)]
167    struct Query {
168      page: u32,
169    }
170
171    #[derive(serde::Serialize)]
172    struct Body {
173      name: String,
174    }
175
176    #[derive(serde::Deserialize)]
177    struct Response {
178      full_name: String,
179    }
180
181    let request = Request::<Body, Query, Response>::builder(&config)
182      .get("/repos/panghu-huang/octocrate")
183      .build();
184
185    let response = request.query(&Query { page: 1 }).send().await.unwrap();
186
187    assert_eq!(response.full_name, "panghu-huang/octocrate");
188  }
189}