httpc_test/
client.rs

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	// region:    --- http calls returning httpc-test Response
47	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	// endregion: --- http calls returning httpc-test Response
71
72	// region:    --- http calls returning typed Deserialized body
73	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	// endregion: --- http calls returning typed Deserialized body
108
109	// region:    --- Cookie
110	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	// endregion: --- Cookie
124
125	// region:    --- Client Privates
126
127	/// Internal implementation for POST, PUT, PATCH
128	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)] // ok for testing lib
149	async fn capture_response(
150		&self,
151		request_method: Method,
152		url: String,
153		reqwest_res: reqwest::Response,
154	) -> Result<Response> {
155		// Note: For now, we will unwrap/panic if fail.
156		//       Might handle this differently in the future.
157		let cookie_store = self.cookie_store.lock().unwrap();
158
159		// Cookies from the client store
160		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	// endregion: --- Client Privates
172}
173
174// region:    --- Post Body
175pub 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
225// endregion: --- Post Body
226
227// region:    --- BaseUrl
228pub 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// endregion: --- BaseUrl