haven 0.1.4

Actix + React + Vite integration for server-rendered applications
Documentation
use http::HeaderMap;
use serde::Serialize;

use crate::protocol::{PageEnvelope, PartialPageEnvelope};

pub const PAGE_VISIT_HEADER: &str = "x-haven-visit";
pub const PAGE_VERSION_HEADER: &str = "x-haven-version";
pub const PAGE_LOCATION_HEADER: &str = "x-haven-location";
pub const PARTIAL_COMPONENT_HEADER: &str = "x-haven-partial-component";
pub const PARTIAL_ONLY_HEADER: &str = "x-haven-partial-only";
pub const PARTIAL_KIND_HEADER: &str = "x-haven-partial-kind";
pub const PARTIAL_RESPONSE_HEADER: &str = "x-haven-partial";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartialReloadKind {
    Props,
    Resources,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialReloadEntry {
    pub kind: PartialReloadKind,
    pub key: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialReloadRequest {
    pub component: String,
    pub entries: Vec<PartialReloadEntry>,
}

pub struct PageProtocol(pub PageEnvelope);
pub struct PartialPageProtocol(pub PartialPageEnvelope);
pub struct ValidationErrors(pub PageEnvelope);

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Redirect {
    pub(crate) status: http::StatusCode,
    pub(crate) location: String,
    pub(crate) hard: bool,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum BrowserStreamEvent<T> {
    Item { data: T },
    Done,
    Error { message: String },
}

pub fn wants_page_protocol(headers: &HeaderMap) -> bool {
    headers
        .get(PAGE_VISIT_HEADER)
        .and_then(|value| value.to_str().ok())
        .map(|value| value.eq_ignore_ascii_case("true"))
        .unwrap_or(false)
}

pub fn page_version(headers: &HeaderMap) -> Option<&str> {
    headers.get(PAGE_VERSION_HEADER)?.to_str().ok()
}

pub fn partial_reload_request(headers: &HeaderMap) -> Option<PartialReloadRequest> {
    if !wants_page_protocol(headers) {
        return None;
    }

    let component = headers.get(PARTIAL_COMPONENT_HEADER)?.to_str().ok()?.trim();
    if component.is_empty() {
        return None;
    }

    let only = headers.get(PARTIAL_ONLY_HEADER)?.to_str().ok()?.trim();
    let kinds = headers.get(PARTIAL_KIND_HEADER)?.to_str().ok()?.trim();
    if only.is_empty() || kinds.is_empty() {
        return None;
    }

    let keys = only
        .split(',')
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let kinds = kinds
        .split(',')
        .map(str::trim)
        .filter(|value| !value.is_empty());

    let entries = keys
        .zip(kinds)
        .filter_map(|(key, kind)| {
            let kind = match kind {
                "props" => PartialReloadKind::Props,
                "resources" => PartialReloadKind::Resources,
                _ => return None,
            };
            Some(PartialReloadEntry {
                kind,
                key: key.to_owned(),
            })
        })
        .collect::<Vec<_>>();

    if entries.is_empty() {
        return None;
    }

    Some(PartialReloadRequest {
        component: component.to_owned(),
        entries,
    })
}

impl Redirect {
    pub fn to(location: impl Into<String>) -> Self {
        Self::see_other(location)
    }

    pub fn see_other(location: impl Into<String>) -> Self {
        Self {
            status: http::StatusCode::SEE_OTHER,
            location: location.into(),
            hard: false,
        }
    }

    pub fn temporary(location: impl Into<String>) -> Self {
        Self {
            status: http::StatusCode::TEMPORARY_REDIRECT,
            location: location.into(),
            hard: false,
        }
    }

    pub fn permanent(location: impl Into<String>) -> Self {
        Self {
            status: http::StatusCode::PERMANENT_REDIRECT,
            location: location.into(),
            hard: false,
        }
    }

    pub fn hard(location: impl Into<String>) -> Self {
        Self {
            status: http::StatusCode::CONFLICT,
            location: location.into(),
            hard: true,
        }
    }

    pub fn location(&self) -> &str {
        &self.location
    }
}