use apimock_config::config::log_config::verbose_config::VerboseConfig;
use apimock_routing::{ParsedRequest, util::http::normalize_url_path};
use console::style;
use http_body_util::BodyExt;
use hyper::header::ORIGIN;
use hyper::{Version, body::Incoming};
use serde_json::{Value, to_string_pretty};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::http_util::content_type_is_application_json;
pub async fn parsed_request_from(
request: hyper::Request<Incoming>,
) -> Result<ParsedRequest, String> {
let (component_parts, body) = request.into_parts();
let body_bytes = match body.boxed().collect().await {
Ok(x) => Some(x.to_bytes()),
Err(err) => {
log::warn!("failed to collect request incoming body: {}", err);
None
}
};
let has_body = body_bytes.as_ref().map(|b| !b.is_empty()).unwrap_or(false);
let body_json = if has_body {
let bytes = body_bytes
.as_ref()
.expect("body_bytes presence checked by has_body");
let raw_body_json = serde_json::from_slice::<Option<Value>>(bytes);
match (
content_type_is_application_json(&component_parts.headers),
raw_body_json,
) {
(Some(true), Err(err)) => {
return Err(format!(
"failed to get json value from request body: {}",
err
));
}
(Some(true), Ok(v)) => v,
(_, Ok(v)) => {
if matches!(
content_type_is_application_json(&component_parts.headers),
Some(false)
) {
log::warn!("request has body but its content-type is not application/json");
} else if content_type_is_application_json(&component_parts.headers).is_none() {
log::warn!("request has body but doesn't have content-type");
}
v
}
(_, Err(_)) => None,
}
} else {
None
};
let url_path = normalize_url_path(component_parts.uri.path(), None);
Ok(ParsedRequest {
url_path,
component_parts,
body_json,
})
}
pub fn capture_in_log(request: &ParsedRequest, verbose: VerboseConfig) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default();
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
let timestamp = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
let version = match request.component_parts.version {
Version::HTTP_3 => "HTTP/3",
Version::HTTP_2 => "HTTP/2",
Version::HTTP_11 => "HTTP/1.1",
_ => "HTTP/1.0 or earlier, or HTTP/4 or later",
};
let origin = request
.component_parts
.headers
.get(ORIGIN)
.and_then(|v| v.to_str().ok());
let mut printed = format!(
"<- {}\n [{}]",
style(request.url_path.as_str()).yellow(),
request.component_parts.method,
);
if let Some(origin) = origin {
printed.push_str(&format!(" [ORIGIN {}]", origin));
}
printed.push_str(&format!(
" [{}] request received (at {} UTC)",
version, timestamp
));
if verbose.header || verbose.body {
printed.push_str("\n");
}
if verbose.header {
let headers = request
.component_parts
.headers
.iter()
.map(|(name, value)| format!("\n{}: {}", name, value.to_str().unwrap_or("<non-utf8>")))
.collect::<String>();
printed.push_str(&format!(
" [request.headers]{}\n",
style(headers).magenta()
));
}
let mut is_verbose_body = false;
if verbose.body {
let query = request.component_parts.uri.query();
if let Some(query) = query {
printed.push_str(&format!(" [request.query] {}\n", query));
is_verbose_body = true;
}
if let Some(request_body_json_value) = &request.body_json {
printed.push_str(" [request.body.json]\n");
let body_str = match to_string_pretty(request_body_json_value) {
Ok(x) => x,
Err(err) => {
log::warn!(
"failed to prettify JSON: {} ({})",
request_body_json_value,
err
);
request_body_json_value.to_string()
}
};
let styled_body_str = body_str
.split("\n")
.map(|s| style(s).green().to_string())
.collect::<Vec<String>>()
.join("\n");
printed.push_str(styled_body_str.as_str());
is_verbose_body = true;
}
}
if verbose.header || is_verbose_body {
printed.push_str("\n");
}
log::info!("{}", printed);
}