ript 0.1.2

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
Documentation
//! The tower / axum extension that drives the current inertia request state.
//!
//! By using an extension to pass data around, we can easily enable share props to work
//! and give a large degree of control to the developer for writing their own middleware
//! that integrates with inertia.

use axum::http::HeaderValue;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::RenderResponse;

/// The request extension itself. Holds all of the current request state.
/// This is modified by the user, and then consumed and turned into a response by
/// the inertia driver.
///
/// This is private and all access is gated through the extractor to ensure that a
/// developer doesn't accidentally take anything out of the extension state and break
/// the contract between the extractor and the driver.
pub struct InertiaExtension {
    /// The accumulated props to set
    props: RwLock<serde_json::Value>,
    /// The accumulated flash props to set
    flash_props: RwLock<Option<serde_json::Value>>,
    /// The props to defer
    deferred_props: RwLock<Vec<&'static str>>,
    /// The props to merge
    merge_props: RwLock<Vec<&'static str>>,
    /// The component to render
    component: RwLock<Option<&'static str>>,
    /// The redirect to perform
    redirect: RwLock<Option<HeaderValue>>,
    /// True if the history should be encrypted
    encrypt_history: AtomicBool,
    /// True if the history should be cleared
    clear_history: AtomicBool,
    /// The version to use for cache busting
    version: RwLock<Option<&'static str>>,
    /// Parts of HTML to sandwich around the props
    html_sandwich: RwLock<(String, String)>,
}

impl Default for InertiaExtension {
    fn default() -> Self {
        Self {
            flash_props: RwLock::new(None),
            props: RwLock::new(serde_json::json!({
                "errors": {}
            })),
            deferred_props: RwLock::new(Vec::new()),
            merge_props: RwLock::new(Vec::new()),
            component: RwLock::new(None),
            redirect: RwLock::new(None),
            encrypt_history: AtomicBool::new(false),
            clear_history: AtomicBool::new(false),
            version: RwLock::new(None),
            html_sandwich: RwLock::new(("".to_owned(), "".to_owned())),
        }
    }
}

impl InertiaExtension {
    /// Add a prop to the response. This will override whatever was set here previously
    pub fn add_prop(&self, key: &'static str, value: serde_json::Value) {
        let mut props = self.props.write();
        props[key] = value;
    }

    pub fn set_version(&self, v: &'static str) {
        let mut version = self.version.write();
        *version = Some(v);
    }

    pub async fn render_html(&self, rr: RenderResponse<'_>) -> String {
        let rr = serde_json::to_string(&rr).expect("props should be serializable");
        let rr = askama_escape::escape(&rr, askama_escape::Html);

        let (head, tail) = &*self.html_sandwich.read();

        format!("{head}{rr}{tail}")
    }

    pub fn add_flash_prop(&self, key: &'static str, value: serde_json::Value) {
        let mut props = self.flash_props.write();

        if props.is_none() {
            *props = Some(serde_json::json!({}));
        }

        // SAFETY: set above, and there is a lock we're holding
        let props = unsafe { Option::unwrap_unchecked(props.as_mut()) };

        props[key] = value;
    }

    pub fn take_flash_props(&self) -> Option<serde_json::Value> {
        let mut props = self.flash_props.write();
        props.take()
    }

    /// Take the props out of the extension. This will return an owned value of all of the
    /// props and clear the internal props value. Only used by the driver.
    pub fn take_props(&self) -> serde_json::Value {
        let mut props = self.props.write();
        props.take()
    }

    /// Extend the props existing with a new set of props. This is effectively a merge operation,
    /// so any conflicting keys will be replaced with the keys provided here.
    pub fn extend_props(&self, props: serde_json::Value) {
        let mut base_props = self.props.write();
        if let (Some(base), serde_json::Value::Object(map)) = (base_props.as_object_mut(), props) {
            for (k, v) in map {
                base.insert(k, v);
            }
        }
    }

    pub fn set_html_sandwich(&self, start: String, end: String) {
        let mut hs = self.html_sandwich.write();
        *hs = (start, end);
    }

    /// Get the component to render
    pub fn component(&self) -> Option<&'static str> {
        *self.component.read()
    }

    /// Set the component to render
    pub fn set_component(&self, component: &'static str) {
        let mut comp = self.component.write();
        *comp = Some(component);
    }

    /// Take the redirect to perform. This will return the redirect and clear the
    /// internal redirect value.
    pub fn take_redirect(&self) -> Option<HeaderValue> {
        let mut red = self.redirect.write();
        red.take()
    }

    /// Set the redirect to perform. This will take priority over the component and
    /// facilitate a redirect. When a redirect is set, any props automatically become
    /// flash props so having a flash provided set up is required to use props with a redirect.
    /// You can use a redirect without a flash provider, but setting any props will cause a panic.
    pub fn set_redirect(&self, redirect: &str) {
        let header = HeaderValue::from_str(redirect).expect("redirect header should be valid");
        let mut red = self.redirect.write();
        *red = Some(header);
    }

    /// Set the history to be encrypted
    pub fn encrypt_history(&self, encrypt: bool) {
        self.encrypt_history.store(encrypt, Ordering::SeqCst);
    }

    /// Get the encrypt history flag
    pub fn get_encrypt_history(&self) -> bool {
        self.encrypt_history.load(Ordering::SeqCst)
    }

    /// Set the history to be cleared
    pub fn clear_history(&self, clear: bool) {
        self.clear_history.store(clear, Ordering::SeqCst);
    }

    /// Get the clear history flag
    pub fn get_clear_history(&self) -> bool {
        self.clear_history.load(Ordering::SeqCst)
    }

    /// Adds a prop to the deferred props. This will indicate to the client that the prop should
    /// be requested after the initial load (which is automatically handled by the inertiajs client
    /// package). This should never be used with share props as it will cause every request to double
    /// load.
    pub fn defer_prop(&self, prop: &'static str) {
        let mut deferred = self.deferred_props.write();
        deferred.push(prop);
    }

    /// Adds a prop to the merge props. This is not to be confused with `extend_props`, which
    /// merges the current props _on the server_. This is actually a flag in the inertia protocol
    /// which indicates, to the client, that a prop should be merged with the previous value set
    /// during a partial reload.
    pub fn merge_prop(&self, prop: &'static str) {
        let mut merge = self.merge_props.write();
        merge.push(prop);
    }

    pub fn version(&self) -> Option<&'static str> {
        *self.version.read()
    }
}