httpc_test/
response.rs

1use crate::cookie::Cookie;
2use crate::{Error, Result};
3use reqwest::{Method, StatusCode};
4use serde::de::DeserializeOwned;
5use serde_json::{to_string_pretty, Value};
6
7#[allow(unused)]
8#[cfg(feature = "color-output")]
9use colored::*;
10#[allow(unused)]
11#[cfg(feature = "color-output")]
12use colored_json::prelude::*;
13use reqwest::header::HeaderMap;
14
15pub struct Response {
16	request_method: Method,
17	request_url: String,
18
19	status: StatusCode,
20	header_map: HeaderMap,
21
22	client_cookies: Vec<Cookie>,
23
24	/// Cookies from the response
25	cookies: Vec<Cookie>,
26	body: Body,
27}
28
29enum Body {
30	Json(Value),
31	Text(String),
32	Other,
33}
34
35#[allow(unused)]
36#[cfg(feature = "color-output")]
37fn get_status_color(status: &StatusCode) -> Color {
38	match status.as_u16() {
39		200..=299 => Color::Green,  // 2xx status codes are successful so we color them green
40		300..=399 => Color::Blue,   // 3xx status codes are for redirection so we color them blue
41		400..=499 => Color::Yellow, // 4xx status codes are client errors so we color them yellow
42		500..=599 => Color::Red,    // 5xx status codes are server errors so we color them red
43		_ => Color::White,          // Anything else we just color white
44	}
45}
46
47#[allow(unused)]
48#[cfg(feature = "color-output")]
49fn get_method_background(method: &Method) -> Color {
50	match *method {
51		Method::GET => Color::TrueColor { r: 223, g: 231, b: 238 },
52		Method::POST => Color::TrueColor { r: 220, g: 233, b: 228 },
53		Method::PUT => Color::TrueColor { r: 238, g: 229, b: 218 },
54		Method::DELETE => Color::TrueColor { r: 238, g: 219, b: 219 },
55		_ => Color::White,
56	}
57}
58
59#[allow(unused)]
60#[cfg(feature = "color-output")]
61fn get_method_color(method: &Method) -> Color {
62	match *method {
63		Method::GET => Color::TrueColor { r: 92, g: 166, b: 241 },
64		Method::POST => Color::TrueColor { r: 59, g: 184, b: 127 },
65		Method::PUT => Color::TrueColor { r: 239, g: 153, b: 46 },
66		Method::DELETE => Color::TrueColor { r: 236, g: 59, b: 59 },
67		_ => Color::White,
68	}
69}
70
71#[allow(unused)]
72#[cfg(feature = "color-output")]
73fn split_and_color_url(url: &str) -> String {
74	let url_struct = url::Url::parse(url).unwrap();
75	let path = url_struct.path();
76	format!("{}", path.purple())
77}
78
79#[allow(unused)]
80#[cfg(feature = "color-output")]
81fn format_method(method: &Method) -> String {
82	format!(" {:<10}", method.to_string())
83}
84
85#[allow(unused)]
86#[cfg(feature = "color-output")]
87const INDENTATION: u8 = 12;
88
89impl Response {
90	pub(crate) async fn from_reqwest_response(
91		request_method: Method,
92		request_url: String,
93		client_cookies: Vec<Cookie>,
94		mut res: reqwest::Response,
95	) -> Result<Response> {
96		let status = res.status();
97
98		// Cookies from response
99		let cookies: Vec<Cookie> = res.cookies().map(Cookie::from).collect();
100
101		// Move the headers into a new HeaderMap
102		let headers = res.headers_mut().drain().filter_map(|(n, v)| n.map(|n| (n, v)));
103		let header_map = HeaderMap::from_iter(headers);
104
105		// Capture the body
106		let ct = header_map.get("content-type").and_then(|v| v.to_str().ok());
107		let body = if let Some(ct) = ct {
108			if ct.starts_with("application/json") {
109				Body::Json(res.json::<Value>().await?)
110			} else if ct.starts_with("text/") {
111				Body::Text(res.text().await?)
112			} else {
113				Body::Other
114			}
115		} else {
116			Body::Other
117		};
118
119		Ok(Response {
120			client_cookies,
121			request_method,
122			request_url,
123			status,
124			header_map,
125			cookies,
126			body,
127		})
128	}
129}
130
131impl Response {
132	// region:    --- Print Methods
133	pub async fn print(&self) -> Result<()> {
134		self.inner_print(true).await
135	}
136
137	pub async fn print_no_body(&self) -> Result<()> {
138		self.inner_print(false).await
139	}
140
141	/// NOTE: For now, does not need to be async, but keeping the option of using async for later.
142	#[allow(unused)]
143	#[cfg(feature = "color-output")]
144	async fn inner_print(&self, body: bool) -> Result<()> {
145		let method_color = get_method_color(&self.request_method);
146		let method_background = get_method_background(&self.request_method);
147		let colored_url = split_and_color_url(&self.request_url);
148		let status_color = get_status_color(&self.status);
149		println!();
150		println!(
151			"{}: {}",
152			format_method(&self.request_method)
153				.bold()
154				.color(method_color)
155				.on_truecolor(50, 50, 50),
156			colored_url
157		);
158		println!(
159			" {:<9} : {} {}",
160			"Status".blue(),
161			self.status.as_str().bold().color(status_color).on_black(),
162			self.status.canonical_reason().unwrap_or_default().color(status_color)
163		);
164
165		// Print the response headers.
166		println!(" {:<9} :", "Headers".blue());
167
168		for (n, v) in self.header_map.iter() {
169			println!("    {}: {}", n.to_string().yellow(), v.to_str().unwrap_or_default());
170		}
171
172		// Print the cookie_store
173		if !self.cookies.is_empty() {
174			println!(" {}:", "Response Cookies".blue());
175			for c in self.cookies.iter() {
176				println!("    {}: {}", c.name.yellow(), c.value.bold());
177			}
178		}
179
180		// Print the cookie_store
181		if !self.client_cookies.is_empty() {
182			println!(" {}:", "Client Cookies".blue());
183			for c in self.client_cookies.iter() {
184				println!("    {}: {}", c.name.yellow(), c.value.bold());
185			}
186		}
187
188		if body {
189			// Print the body (json pretty print if json type)
190			println!("{}:", "Response Body".blue());
191			match &self.body {
192				Body::Json(val) => println!("{}", to_string_pretty(val)?.to_colored_json_auto()?),
193				Body::Text(val) => println!("    {}", val.color(status_color)),
194				_ => (),
195			}
196		}
197
198		println!("\n");
199		Ok(())
200	}
201
202	#[cfg(not(feature = "color-output"))]
203	async fn inner_print(&self, body: bool) -> Result<()> {
204		println!();
205		println!("=== Response for {} {}", self.request_method, &self.request_url);
206
207		println!(
208			"=> {:<15}: {} {}",
209			"Status",
210			self.status.as_str(),
211			self.status.canonical_reason().unwrap_or_default()
212		);
213
214		// Print the response headers.
215		println!("=> {:<15}:", "Headers");
216
217		for (n, v) in self.header_map.iter() {
218			println!("   {}: {}", n, v.to_str().unwrap_or_default());
219		}
220
221		// Print the cookie_store
222		if !self.cookies.is_empty() {
223			println!("=> {:<15}:", "Response Cookies");
224			for c in self.cookies.iter() {
225				println!("   {}: {}", c.name, c.value);
226			}
227		}
228
229		// Print the cookie_store
230		if !self.client_cookies.is_empty() {
231			println!("=> {:<15}:", "Client Cookies");
232			for c in self.client_cookies.iter() {
233				println!("   {}: {}", c.name, c.value);
234			}
235		}
236
237		if body {
238			// Print the body (json pretty print if json type)
239			println!("=> {:<15}:", "Response Body");
240			match &self.body {
241				Body::Json(val) => println!("{}", to_string_pretty(val)?),
242				Body::Text(val) => println!("{}", val),
243				_ => (),
244			}
245		}
246
247		println!("===\n");
248		Ok(())
249	}
250
251	// endregion: --- Print Methods
252
253	// region:    --- Headers
254	pub fn header_all(&self, name: &str) -> Vec<String> {
255		self.header_map
256			.get_all(name)
257			.iter()
258			.filter_map(|v| v.to_str().map(|v| v.to_string()).ok())
259			.collect()
260	}
261
262	pub fn header(&self, name: &str) -> Option<String> {
263		self.header_map.get(name).and_then(|v| v.to_str().map(|v| v.to_string()).ok())
264	}
265	// endregion: --- Headers
266
267	// region:    --- Status Code
268	/// Return the Response status code
269	pub fn status(&self) -> StatusCode {
270		self.status
271	}
272	// endregion: --- Status Code
273
274	// region:    --- Response Cookie
275	/// Return the cookie that has been set for this http response.
276	pub fn res_cookie(&self, name: &str) -> Option<&Cookie> {
277		self.cookies.iter().find(|c| c.name == name)
278	}
279
280	/// Return the cookie value that has been set for this http response.
281	pub fn res_cookie_value(&self, name: &str) -> Option<String> {
282		self.cookies.iter().find(|c| c.name == name).map(|c| c.value.clone())
283	}
284	// endregion: --- Response Cookie
285
286	// region:    --- Client Cookies
287	/// Return the client httpc-test Cookie for a given name.
288	/// Note: The response.client_cookies are the captured client cookies
289	///       at the time of the response.
290	pub fn client_cookie(&self, name: &str) -> Option<&Cookie> {
291		self.client_cookies.iter().find(|c| c.name == name)
292	}
293
294	/// Return the client cookie value as String for a given name.
295	/// Note: The response.client_cookies are the captured client cookies
296	///       at the time of the response.
297	pub fn client_cookie_value(&self, name: &str) -> Option<String> {
298		self.client_cookies.iter().find(|c| c.name == name).map(|c| c.value.clone())
299	}
300	// endregion: --- Client Cookies
301
302	// region:    --- Body
303	pub fn json_body(&self) -> Result<Value> {
304		match &self.body {
305			Body::Json(val) => Ok(val.clone()),
306			_ => Err(Error::Static("No json body")),
307		}
308	}
309
310	pub fn text_body(&self) -> Result<String> {
311		match &self.body {
312			Body::Text(val) => Ok(val.clone()),
313			_ => Err(Error::Static("No text body")),
314		}
315	}
316
317	pub fn json_value<T>(&self, pointer: &str) -> Result<T>
318	where
319		T: DeserializeOwned,
320	{
321		let Body::Json(body) = &self.body else {
322			return Err(Error::Static("No json body"));
323		};
324
325		let value = body.pointer(pointer).ok_or_else(|| Error::NoJsonValueFound {
326			json_pointer: pointer.to_string(),
327		})?;
328
329		Ok(serde_json::from_value::<T>(value.clone())?)
330	}
331
332	pub fn json_body_as<T>(&self) -> Result<T>
333	where
334		T: DeserializeOwned,
335	{
336		self.json_body()
337			.and_then(|val| serde_json::from_value::<T>(val).map_err(Error::SerdeJson))
338	}
339	// endregion: --- Body
340}