veer 0.1.1

Inertia.js v3 server-side protocol superset for Rust
Documentation
//! Per-request data parsed from Inertia headers.

use http::{HeaderMap, Method};
use std::collections::HashSet;

/// Request information needed to drive the Inertia protocol.
#[derive(Debug, Clone)]
pub struct RequestInfo {
    /// HTTP method.
    pub method: Method,
    /// Full URL the client is currently at (path + query).
    pub url: String,
    /// Value of the `Referer` header, if present.
    ///
    /// Used by [`crate::inertia::Inertia::back()`] to redirect the client to the
    /// previous page. Falls back to `"/"` when absent.
    pub referer: Option<String>,
    /// `true` iff `X-Inertia: true` was set.
    pub is_inertia: bool,
    /// Client-reported asset version, if any.
    pub client_version: Option<String>,
    /// Component being partially reloaded, if any.
    pub partial_component: Option<String>,
    /// Allowlist of prop keys for a partial reload.
    pub partial_only: HashSet<String>,
    /// Denylist of prop keys for a partial reload.
    pub partial_except: HashSet<String>,
    /// Keys the client wants reset (clear merge state for these).
    pub reset: HashSet<String>,
}

impl RequestInfo {
    /// Parse headers + method + url into a [`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),
        }
    }

    /// Returns `true` if the request is a partial reload (component header set + only/except non-empty).
    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());
    }
}