use std::io::{self, Read};
use encoding_rs::{Encoding, UTF_8};
use encoding_rs_io::DecodeReaderBytesBuilder;
use mime::Mime;
use reqwest::blocking::{Request, Response};
use reqwest::header::{
HeaderMap, HeaderName, HeaderValue, ACCEPT, CONTENT_LENGTH, CONTENT_TYPE, HOST,
};
use crate::{
formatting::{get_json_formatter, Highlighter},
utils::{copy_largebuf, get_content_type, test_mode, valid_json, ContentType},
};
use crate::{Buffer, Pretty, Theme};
const MULTIPART_SUPPRESSOR: &str = concat!(
"+--------------------------------------------+\n",
"| NOTE: multipart data not shown in terminal |\n",
"+--------------------------------------------+\n",
"\n"
);
const BINARY_SUPPRESSOR: &str = concat!(
"+-----------------------------------------+\n",
"| NOTE: binary data not shown in terminal |\n",
"+-----------------------------------------+\n",
"\n"
);
pub struct Printer {
indent_json: bool,
color: bool,
theme: Theme,
sort_headers: bool,
stream: bool,
buffer: Buffer,
}
impl Printer {
pub fn new(pretty: Option<Pretty>, theme: Option<Theme>, stream: bool, buffer: Buffer) -> Self {
let pretty = pretty.unwrap_or_else(|| Pretty::from(&buffer));
let theme = theme.unwrap_or(Theme::auto);
Printer {
indent_json: pretty.format(),
sort_headers: pretty.format(),
color: pretty.color(),
stream,
theme,
buffer,
}
}
fn with_unguarded_highlighter(
&mut self,
syntax: &'static str,
code: impl FnOnce(&mut Highlighter) -> io::Result<()>,
) -> io::Result<()> {
let mut highlighter =
Highlighter::new(syntax, self.theme, Box::new(self.buffer.unguarded()));
code(&mut highlighter)?;
highlighter.finish()
}
fn print_text(&mut self, text: &str) -> io::Result<()> {
self.buffer.unguarded().write_all(text.as_bytes())
}
fn print_colorized_text(&mut self, text: &str, syntax: &'static str) -> io::Result<()> {
self.with_unguarded_highlighter(syntax, |highlighter| highlighter.highlight(text))
}
fn print_syntax_text(&mut self, text: &str, syntax: &'static str) -> io::Result<()> {
if self.color {
self.print_colorized_text(text, syntax)
} else {
self.print_text(text)
}
}
fn print_json_text(&mut self, text: &str, check_valid: bool) -> io::Result<()> {
if !self.indent_json {
return self.print_syntax_text(text, "json");
}
if check_valid && !valid_json(text) {
return self.print_syntax_text(text, "json");
}
if self.color {
let mut buf = Vec::new();
get_json_formatter().format_stream_unbuffered(&mut text.as_bytes(), &mut buf)?;
let text = String::from_utf8_lossy(&buf);
self.print_colorized_text(&text, "json")
} else {
get_json_formatter()
.format_stream_unbuffered(&mut text.as_bytes(), &mut self.buffer.unguarded())
}
}
fn print_body_text(&mut self, content_type: Option<ContentType>, body: &str) -> io::Result<()> {
match content_type {
Some(ContentType::Json) => self.print_json_text(body, true),
Some(ContentType::Xml) => self.print_syntax_text(body, "xml"),
Some(ContentType::Html) => self.print_syntax_text(body, "html"),
Some(ContentType::Css) => self.print_syntax_text(body, "css"),
Some(ContentType::Text) | Some(ContentType::JavaScript) if valid_json(body) => {
self.print_json_text(body, false)
}
Some(ContentType::JavaScript) => self.print_syntax_text(body, "js"),
_ => self.buffer.print(body),
}
}
fn with_guarded_highlighter(
&mut self,
syntax: &'static str,
code: impl FnOnce(&mut Highlighter) -> io::Result<()>,
) -> io::Result<()> {
let theme = self.theme; self.buffer.with_guard(|guard| {
let mut highlighter = Highlighter::new(syntax, theme, Box::new(guard));
code(&mut highlighter)?;
highlighter.finish()
})
}
fn print_stream(&mut self, reader: &mut impl Read) -> io::Result<()> {
self.buffer
.with_guard(|mut guard| copy_largebuf(reader, &mut guard))
}
fn print_colorized_stream(
&mut self,
stream: &mut impl Read,
syntax: &'static str,
) -> io::Result<()> {
self.with_guarded_highlighter(syntax, |highlighter| {
copy_largebuf(stream, &mut highlighter.linewise())?;
Ok(())
})
}
fn print_syntax_stream(
&mut self,
stream: &mut impl Read,
syntax: &'static str,
) -> io::Result<()> {
if self.color {
self.print_colorized_stream(stream, syntax)
} else {
self.print_stream(stream)
}
}
fn print_json_stream(&mut self, stream: &mut impl Read) -> io::Result<()> {
if !self.indent_json {
self.print_syntax_stream(stream, "json")
} else if self.color {
self.with_guarded_highlighter("json", |highlighter| {
get_json_formatter().format_stream_unbuffered(stream, &mut highlighter.linewise())
})
} else {
self.buffer.with_guard(|mut guard| {
get_json_formatter().format_stream_unbuffered(stream, &mut guard)
})
}
}
fn print_body_stream(
&mut self,
content_type: Option<ContentType>,
body: &mut impl Read,
) -> io::Result<()> {
match content_type {
Some(ContentType::Json) => self.print_json_stream(body),
Some(ContentType::Xml) => self.print_syntax_stream(body, "xml"),
Some(ContentType::Html) => self.print_syntax_stream(body, "html"),
Some(ContentType::Css) => self.print_syntax_stream(body, "css"),
Some(ContentType::JavaScript) => self.print_syntax_stream(body, "js"),
_ => self.print_stream(body),
}
}
fn print_headers(&mut self, text: &str) -> io::Result<()> {
if self.color {
self.print_colorized_text(text, "http")
} else {
self.buffer.print(text)
}
}
fn headers_to_string(&self, headers: &HeaderMap, sort: bool) -> String {
let mut headers: Vec<(&HeaderName, &HeaderValue)> = headers.iter().collect();
if sort {
headers.sort_by_key(|(name, _)| name.as_str());
}
let mut header_string = String::new();
for (key, value) in headers {
header_string.push_str(key.as_str());
header_string.push_str(": ");
match value.to_str() {
Ok(value) => header_string.push_str(value),
Err(_) => header_string.push_str(&format!("{:?}", value)),
}
header_string.push('\n');
}
header_string.pop();
header_string
}
pub fn print_request_headers(&mut self, request: &Request) -> io::Result<()> {
let method = request.method();
let url = request.url();
let query_string = url.query().map_or(String::from(""), |q| ["?", q].concat());
let version = reqwest::Version::HTTP_11;
let mut headers = request.headers().clone();
headers
.entry(ACCEPT)
.or_insert_with(|| HeaderValue::from_static("*/*"));
if let Some(body) = request.body().and_then(|body| body.as_bytes()) {
headers
.entry(CONTENT_LENGTH)
.or_insert_with(|| body.len().into());
}
if let Some(host) = request.url().host_str() {
headers.entry(HOST).or_insert_with(|| {
if test_mode() {
HeaderValue::from_str("http.mock")
} else if let Some(port) = request.url().port() {
HeaderValue::from_str(&format!("{}:{}", host, port))
} else {
HeaderValue::from_str(host)
}
.expect("hostname should already be validated/parsed")
});
}
let request_line = format!("{} {}{} {:?}\n", method, url.path(), query_string, version);
let headers = &self.headers_to_string(&headers, self.sort_headers);
self.print_headers(&(request_line + &headers))?;
self.buffer.print("\n\n")?;
Ok(())
}
pub fn print_response_headers(&mut self, response: &Response) -> io::Result<()> {
let version = response.version();
let status = response.status();
let headers = response.headers();
let status_line = format!("{:?} {}\n", version, status);
let headers = self.headers_to_string(headers, self.sort_headers);
self.print_headers(&(status_line + &headers))?;
self.buffer.print("\n\n")?;
Ok(())
}
pub fn print_request_body(&mut self, request: &Request) -> io::Result<()> {
match get_content_type(&request.headers()) {
Some(ContentType::Multipart) => {
self.buffer.print(MULTIPART_SUPPRESSOR)?;
}
content_type => {
if let Some(body) = request.body().and_then(|b| b.as_bytes()) {
if body.contains(&b'\0') {
self.buffer.print(BINARY_SUPPRESSOR)?;
} else {
self.print_body_text(content_type, &String::from_utf8_lossy(body))?;
self.buffer.print("\n")?;
}
self.buffer.print("\n")?;
}
}
}
Ok(())
}
pub fn print_response_body(&mut self, mut response: Response) -> anyhow::Result<()> {
let content_type = get_content_type(&response.headers());
if !self.buffer.is_terminal() {
self.print_body_stream(content_type, &mut response)?;
} else if self.stream {
match self.print_body_stream(content_type, &mut decode_stream(&mut response)) {
Ok(_) => {
self.buffer.print("\n")?;
}
Err(err) if err.kind() == io::ErrorKind::InvalidData => {
if self.color {
self.buffer.print("\x1b[0m")?;
}
self.buffer.print(BINARY_SUPPRESSOR)?;
}
Err(err) => return Err(err.into()),
}
} else {
let text = response.text()?;
if text.contains('\0') {
self.buffer.print(BINARY_SUPPRESSOR)?;
return Ok(());
}
self.print_body_text(content_type, &text)?;
self.buffer.print("\n")?;
}
Ok(())
}
}
fn decode_stream(response: &mut Response) -> impl Read + '_ {
let content_type = response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let encoding_name = content_type
.as_ref()
.and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
.unwrap_or("utf-8");
let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
DecodeReaderBytesBuilder::new()
.encoding(Some(encoding))
.build(response)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{buffer::Buffer, cli::Cli, vec_of_strings};
use assert_matches::assert_matches;
fn run_cmd(args: impl IntoIterator<Item = String>, is_stdout_tty: bool) -> Printer {
let args = Cli::from_iter_safe(args).unwrap();
let buffer = Buffer::new(args.download, &args.output, is_stdout_tty).unwrap();
Printer::new(args.pretty, args.style, false, buffer)
}
fn temp_path(filename: &str) -> String {
let mut dir = std::env::temp_dir();
dir.push(filename);
dir.to_str().unwrap().to_owned()
}
#[test]
fn test_1() {
let p = run_cmd(vec_of_strings!["xh", "httpbin.org/get"], true);
assert_eq!(p.color, true);
assert_matches!(p.buffer, Buffer::Stdout(..));
}
#[test]
fn test_2() {
let p = run_cmd(vec_of_strings!["xh", "httpbin.org/get"], false);
assert_eq!(p.color, false);
assert_matches!(p.buffer, Buffer::Redirect(..));
}
#[test]
fn test_3() {
let output = temp_path("temp3");
let p = run_cmd(vec_of_strings!["xh", "httpbin.org/get", "-o", output], true);
assert_eq!(p.color, false);
assert_matches!(p.buffer, Buffer::File(_));
}
#[test]
fn test_4() {
let output = temp_path("temp4");
let p = run_cmd(
vec_of_strings!["xh", "httpbin.org/get", "-o", output],
false,
);
assert_eq!(p.color, false);
assert_matches!(p.buffer, Buffer::File(_));
}
#[test]
fn test_5() {
let p = run_cmd(vec_of_strings!["xh", "httpbin.org/get", "-d"], true);
assert_eq!(p.color, true);
assert_matches!(p.buffer, Buffer::Stderr(..));
}
#[test]
fn test_6() {
let p = run_cmd(vec_of_strings!["xh", "httpbin.org/get", "-d"], false);
assert_eq!(p.color, true);
assert_matches!(p.buffer, Buffer::Stderr(..));
}
#[test]
fn test_7() {
let output = temp_path("temp7");
let p = run_cmd(
vec_of_strings!["xh", "httpbin.org/get", "-d", "-o", output],
true,
);
assert_eq!(p.color, true);
assert_matches!(p.buffer, Buffer::Stderr(..));
}
#[test]
fn test_8() {
let output = temp_path("temp8");
let p = run_cmd(
vec_of_strings!["xh", "httpbin.org/get", "-d", "-o", output],
false,
);
assert_eq!(p.color, true);
assert_matches!(p.buffer, Buffer::Stderr(..));
}
}