1use crate::cookie::{from_tower_cookie_deref, Cookie};
2use crate::{Error, Response, Result};
3use reqwest::Method;
4use reqwest_cookie_store::CookieStoreMutex;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7use std::sync::Arc;
8
9pub struct Client {
10 base_url: Option<String>,
11 cookie_store: Arc<CookieStoreMutex>,
12 reqwest_client: reqwest::Client,
13}
14
15impl Client {
16 pub fn cookie_store(&self) -> Arc<CookieStoreMutex> {
17 self.cookie_store.clone()
18 }
19 pub fn reqwest_client(&self) -> &reqwest::Client {
20 &self.reqwest_client
21 }
22}
23
24pub fn new_client(base_url: impl Into<BaseUrl>) -> Result<Client> {
25 let reqwest_builder = reqwest::Client::builder();
26
27 new_client_with_reqwest(base_url, reqwest_builder)
28}
29
30pub fn new_client_with_reqwest(
31 base_url: impl Into<BaseUrl>,
32 reqwest_builder: reqwest::ClientBuilder,
33) -> Result<Client> {
34 let base_url = base_url.into().into();
35 let cookie_store = Arc::new(CookieStoreMutex::default());
36 let reqwest_client = reqwest_builder.cookie_provider(cookie_store.clone()).build()?;
37
38 Ok(Client {
39 base_url,
40 cookie_store,
41 reqwest_client,
42 })
43}
44
45impl Client {
46 pub async fn do_get(&self, url: &str) -> Result<Response> {
48 let url = self.compose_url(url);
49 let reqwest_res = self.reqwest_client.get(&url).send().await?;
50 self.capture_response(Method::GET, url, reqwest_res).await
51 }
52
53 pub async fn do_delete(&self, url: &str) -> Result<Response> {
54 let url = self.compose_url(url);
55 let reqwest_res = self.reqwest_client.delete(&url).send().await?;
56 self.capture_response(Method::DELETE, url, reqwest_res).await
57 }
58
59 pub async fn do_post(&self, url: &str, content: impl Into<PostContent>) -> Result<Response> {
60 self.do_push(Method::POST, url, content.into()).await
61 }
62
63 pub async fn do_put(&self, url: &str, content: impl Into<PostContent>) -> Result<Response> {
64 self.do_push(Method::PUT, url, content.into()).await
65 }
66
67 pub async fn do_patch(&self, url: &str, content: impl Into<PostContent>) -> Result<Response> {
68 self.do_push(Method::PATCH, url, content.into()).await
69 }
70 pub async fn get<T>(&self, url: &str) -> Result<T>
74 where
75 T: DeserializeOwned,
76 {
77 self.do_get(url).await.and_then(|res| res.json_body_as::<T>())
78 }
79
80 pub async fn delete<T>(&self, url: &str) -> Result<T>
81 where
82 T: DeserializeOwned,
83 {
84 self.do_delete(url).await.and_then(|res| res.json_body_as::<T>())
85 }
86
87 pub async fn post<T>(&self, url: &str, content: impl Into<PostContent>) -> Result<T>
88 where
89 T: DeserializeOwned,
90 {
91 self.do_post(url, content).await.and_then(|res| res.json_body_as::<T>())
92 }
93
94 pub async fn put<T>(&self, url: &str, content: impl Into<PostContent>) -> Result<T>
95 where
96 T: DeserializeOwned,
97 {
98 self.do_put(url, content).await.and_then(|res| res.json_body_as::<T>())
99 }
100
101 pub async fn patch<T>(&self, url: &str, content: impl Into<PostContent>) -> Result<T>
102 where
103 T: DeserializeOwned,
104 {
105 self.do_patch(url, content).await.and_then(|res| res.json_body_as::<T>())
106 }
107 pub fn cookie(&self, name: &str) -> Option<Cookie> {
111 let cookie_store = self.cookie_store.lock().unwrap();
112 let cookie = cookie_store
113 .iter_any()
114 .find(|c| c.name() == name)
115 .map(|c| from_tower_cookie_deref(c));
116
117 cookie
118 }
119
120 pub fn cookie_value(&self, name: &str) -> Option<String> {
121 self.cookie(name).map(|c| c.value)
122 }
123 async fn do_push(&self, method: Method, url: &str, content: PostContent) -> Result<Response> {
129 let url = self.compose_url(url);
130 if !matches!(method, Method::POST | Method::PUT | Method::PATCH) {
131 return Err(Error::NotSupportedMethodForPush { given_method: method });
132 }
133 let reqwest_res = match content {
134 PostContent::Json(value) => self.reqwest_client.request(method.clone(), &url).json(&value).send().await?,
135 PostContent::Text { content_type, body } => {
136 self.reqwest_client
137 .request(method.clone(), &url)
138 .body(body)
139 .header("content-type", content_type)
140 .send()
141 .await?
142 }
143 };
144
145 self.capture_response(method, url, reqwest_res).await
146 }
147
148 #[allow(clippy::await_holding_lock)] async fn capture_response(
150 &self,
151 request_method: Method,
152 url: String,
153 reqwest_res: reqwest::Response,
154 ) -> Result<Response> {
155 let cookie_store = self.cookie_store.lock().unwrap();
158
159 let client_cookies: Vec<Cookie> = cookie_store.iter_any().map(|c| from_tower_cookie_deref(c)).collect();
161
162 Response::from_reqwest_response(request_method, url, client_cookies, reqwest_res).await
163 }
164
165 fn compose_url(&self, url: &str) -> String {
166 match &self.base_url {
167 Some(base_url) => format!("{base_url}{url}"),
168 None => url.to_string(),
169 }
170 }
171 }
173
174pub enum PostContent {
176 Json(Value),
177 Text { body: String, content_type: &'static str },
178}
179impl From<Value> for PostContent {
180 fn from(val: Value) -> Self {
181 PostContent::Json(val)
182 }
183}
184impl From<String> for PostContent {
185 fn from(val: String) -> Self {
186 PostContent::Text {
187 content_type: "text/plain",
188 body: val,
189 }
190 }
191}
192impl From<&String> for PostContent {
193 fn from(val: &String) -> Self {
194 PostContent::Text {
195 content_type: "text/plain",
196 body: val.to_string(),
197 }
198 }
199}
200
201impl From<&str> for PostContent {
202 fn from(val: &str) -> Self {
203 PostContent::Text {
204 content_type: "text/plain",
205 body: val.to_string(),
206 }
207 }
208}
209
210impl From<(String, &'static str)> for PostContent {
211 fn from((body, content_type): (String, &'static str)) -> Self {
212 PostContent::Text { body, content_type }
213 }
214}
215
216impl From<(&str, &'static str)> for PostContent {
217 fn from((body, content_type): (&str, &'static str)) -> Self {
218 PostContent::Text {
219 body: body.to_string(),
220 content_type,
221 }
222 }
223}
224
225pub struct BaseUrl(Option<String>);
229
230impl From<&str> for BaseUrl {
231 fn from(val: &str) -> Self {
232 BaseUrl(Some(val.to_string()))
233 }
234}
235impl From<String> for BaseUrl {
236 fn from(val: String) -> Self {
237 BaseUrl(Some(val))
238 }
239}
240impl From<&String> for BaseUrl {
241 fn from(val: &String) -> Self {
242 BaseUrl(Some(val.to_string()))
243 }
244}
245impl From<BaseUrl> for Option<String> {
246 fn from(val: BaseUrl) -> Self {
247 val.0
248 }
249}
250impl From<Option<String>> for BaseUrl {
251 fn from(val: Option<String>) -> Self {
252 BaseUrl(val)
253 }
254}
255