cronback_client/
client.rs1use async_trait::async_trait;
2use http::header::{self, USER_AGENT};
3use reqwest::{IntoUrl, RequestBuilder};
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use url::Url;
7
8use crate::constants::{BASE_URL_ENV, DEFAULT_BASE_URL};
9use crate::{Error, Response, Result};
10
11#[derive(Clone)]
23pub struct Client {
24 http_client: reqwest::Client,
25 config: ClientConfig,
26}
27
28#[async_trait]
29pub trait RequestRunner: Sync + Send {
30 fn prepare_request(
31 &self,
32 method: http::Method,
33 path: Url,
34 ) -> Result<RequestBuilder>;
35
36 fn make_url(&self, path: &str) -> Result<Url>;
37
38 fn prepare_request_with_body<B>(
39 &self,
40 method: http::Method,
41 path: Url,
42 body: B,
43 ) -> Result<RequestBuilder>
44 where
45 B: Serialize + std::fmt::Debug,
46 {
47 Ok(self.prepare_request(method, path)?.json(&body))
48 }
49
50 async fn process_response<T>(
51 &self,
52 response: reqwest::Response,
53 ) -> Result<Response<T>>
54 where
55 T: DeserializeOwned + Send,
56 {
57 Response::from_raw_response(response).await
58 }
59
60 async fn run<T>(
61 &self,
62 method: http::Method,
63 path: Url,
64 ) -> Result<Response<T>>
65 where
66 T: DeserializeOwned + Send,
67 {
68 let request = self.prepare_request(method, path)?;
69 let resp = request.send().await?;
70 self.process_response(resp).await
71 }
72
73 async fn run_with_body<T, B>(
74 &self,
75 method: http::Method,
76 path: Url,
77 body: B,
78 ) -> Result<Response<T>>
79 where
80 T: DeserializeOwned + Send,
81 B: Serialize + std::fmt::Debug + Send,
82 {
83 let request = self.prepare_request_with_body(method, path, body)?;
84 let resp = request.send().await?;
85 self.process_response(resp).await
86 }
87}
88
89#[must_use]
96#[derive(Default, Clone)]
97pub struct ClientBuilder {
98 config: Config,
99}
100
101impl ClientBuilder {
102 pub fn new() -> Self {
105 Self {
106 config: Config::default(),
107 }
108 }
109
110 pub fn base_url<T: IntoUrl>(mut self, base_url: T) -> Result<Self> {
111 let mut base_url = base_url.into_url()?;
112 base_url.set_query(None);
114 self.config.base_url = Some(base_url);
115 Ok(self)
116 }
117
118 pub fn secret_token(mut self, secret_token: String) -> Self {
119 self.config.secret_token = Some(secret_token);
120 self
121 }
122
123 #[cfg(feature = "admin")]
124 pub fn on_behalf_of(mut self, project_id: String) -> Self {
130 self.config.on_behalf_of = Some(project_id);
131 self
132 }
133
134 pub fn build(self) -> Result<Client> {
136 let user_agent = format!(
137 "rust-{}-{}-{}",
138 env!("CARGO_PKG_VERSION"),
139 std::env::consts::OS,
140 std::env::consts::ARCH,
141 );
142
143 let mut headers = header::HeaderMap::new();
144 headers.insert(
145 USER_AGENT,
146 header::HeaderValue::from_str(&user_agent).expect("User-Agent"),
147 );
148
149 if let Some(prj) = &self.config.on_behalf_of {
150 headers.insert(
151 "X-On-Behalf-Of",
152 header::HeaderValue::from_str(prj).expect("X-On-Behalf-Of"),
153 );
154 }
155
156 let http_client = match self.config.reqwest_client {
157 | Some(c) => c,
158 | None => {
159 reqwest::ClientBuilder::new()
160 .redirect(reqwest::redirect::Policy::none())
161 .default_headers(headers)
162 .build()?
163 }
164 };
165
166 let base_url = match self.config.base_url {
167 | Some(c) => c,
168 | None => {
169 std::env::var(BASE_URL_ENV)
172 .ok()
173 .map(|base_url| Url::parse(&base_url))
174 .unwrap_or(Ok(DEFAULT_BASE_URL.clone()))
175 .expect("Config::default()")
176 }
177 };
178 Ok(Client {
179 http_client,
180 config: ClientConfig {
181 base_url,
182 secret_token: self
183 .config
184 .secret_token
185 .ok_or(Error::SecretTokenRequired)?,
186 },
187 })
188 }
189
190 pub fn reqwest_client(mut self, c: reqwest::Client) -> Self {
194 self.config.reqwest_client = Some(c);
195 self
196 }
197}
198
199impl Client {
200 pub fn new() -> Self {
211 Self::builder().build().expect("Client::new()")
212 }
213
214 pub fn builder() -> ClientBuilder {
218 ClientBuilder::new()
219 }
220}
221
222impl Default for Client {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228#[derive(Default, Clone)]
229struct Config {
230 base_url: Option<Url>,
231 secret_token: Option<String>,
232 on_behalf_of: Option<String>,
233 reqwest_client: Option<reqwest::Client>,
234}
235
236#[derive(Clone)]
237pub(crate) struct ClientConfig {
238 pub base_url: Url,
239 secret_token: String,
240}
241
242const _: () = {
244 fn assert_send<T: Send + Sync>() {}
245 let _ = assert_send::<Client>;
246};
247
248#[async_trait]
249impl RequestRunner for Client {
250 fn make_url(&self, path: &str) -> Result<Url> {
251 Ok(self.config.base_url.join(path)?)
252 }
253
254 fn prepare_request(
255 &self,
256 method: http::Method,
257 url: Url,
258 ) -> Result<RequestBuilder> {
259 let request = self
260 .http_client
261 .request(method, url)
262 .bearer_auth(&self.config.secret_token);
263
264 Ok(request)
265 }
266}
267
268#[cfg(test)]
269mod tests {}