use http::{HeaderMap, Method};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct RequestInfo {
pub method: Method,
pub url: String,
pub referer: Option<String>,
pub is_inertia: bool,
pub client_version: Option<String>,
pub partial_component: Option<String>,
pub partial_only: HashSet<String>,
pub partial_except: HashSet<String>,
pub reset: HashSet<String>,
}
impl RequestInfo {
pub fn from_parts(method: Method, url: String, headers: &HeaderMap) -> Self {
fn split_csv(headers: &HeaderMap, name: &http::HeaderName) -> HashSet<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(|s| {
s.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
})
.unwrap_or_default()
}
let is_inertia = headers
.get(&crate::headers::X_INERTIA)
.and_then(|v| v.to_str().ok())
== Some("true");
let client_version = headers
.get(&crate::headers::X_INERTIA_VERSION)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let partial_component = headers
.get(&crate::headers::X_INERTIA_PARTIAL_COMPONENT)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let referer = headers
.get(http::header::REFERER)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
Self {
method,
url,
referer,
is_inertia,
client_version,
partial_component,
partial_only: split_csv(headers, &crate::headers::X_INERTIA_PARTIAL_DATA),
partial_except: split_csv(headers, &crate::headers::X_INERTIA_PARTIAL_EXCEPT),
reset: split_csv(headers, &crate::headers::X_INERTIA_RESET),
}
}
pub fn is_partial(&self) -> bool {
self.partial_component.is_some()
&& (!self.partial_only.is_empty() || !self.partial_except.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
fn hv(s: &str) -> HeaderValue {
HeaderValue::from_str(s).unwrap()
}
#[test]
fn plain_request_is_not_inertia() {
let info = RequestInfo::from_parts(Method::GET, "/".into(), &HeaderMap::new());
assert!(!info.is_inertia);
assert!(info.client_version.is_none());
assert!(info.partial_only.is_empty());
assert!(!info.is_partial());
assert!(info.referer.is_none());
}
#[test]
fn referer_parsed_from_header() {
let mut h = HeaderMap::new();
h.insert(http::header::REFERER, hv("https://example.com/previous"));
let info = RequestInfo::from_parts(Method::POST, "/submit".into(), &h);
assert_eq!(
info.referer.as_deref(),
Some("https://example.com/previous")
);
}
#[test]
fn referer_absent_when_header_missing() {
let info = RequestInfo::from_parts(Method::GET, "/page".into(), &HeaderMap::new());
assert!(info.referer.is_none());
}
#[test]
fn inertia_xhr_request_parsed() {
let mut h = HeaderMap::new();
h.insert(&crate::headers::X_INERTIA, hv("true"));
h.insert(&crate::headers::X_INERTIA_VERSION, hv("abc123"));
let info = RequestInfo::from_parts(Method::GET, "/users".into(), &h);
assert!(info.is_inertia);
assert_eq!(info.client_version.as_deref(), Some("abc123"));
}
#[test]
fn partial_reload_parses_only_and_except() {
let mut h = HeaderMap::new();
h.insert(&crate::headers::X_INERTIA, hv("true"));
h.insert(
&crate::headers::X_INERTIA_PARTIAL_COMPONENT,
hv("Users/Index"),
);
h.insert(&crate::headers::X_INERTIA_PARTIAL_DATA, hv("users, stats"));
h.insert(&crate::headers::X_INERTIA_PARTIAL_EXCEPT, hv("auth"));
let info = RequestInfo::from_parts(Method::GET, "/users".into(), &h);
assert_eq!(info.partial_component.as_deref(), Some("Users/Index"));
assert!(info.partial_only.contains("users"));
assert!(info.partial_only.contains("stats"));
assert!(info.partial_except.contains("auth"));
assert!(info.is_partial());
}
}