ript 0.1.2

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
Documentation
//! Prop logic for Inertia. This defines how to determine what props to calculate and render,
//! and which ones to send back. This tries to maintain very close API partity with laravel's
//! inertia implementation.

use std::{convert::Infallible, ops::ControlFlow};

use axum::{http::Method, response::IntoResponse};
use bon::{Builder, bon, builder};
use serde::Serialize;

use crate::{extension::InertiaExtension, inertia::RequestHeaders};

/// Attributes for a prop. This determines how a prop should be handled during a full or partial reload.
#[derive(Default, Builder)]
#[builder(state_mod(vis = "pub(crate)"))]
pub struct Prop<F> {
    // the inner value of the prop, which is an async closure that will produce the value
    // if the eval tells us to do so
    #[builder(start_fn)]
    value: F,

    /// This prop should only be rendered if explicitly requested in a partial reload.
    #[builder(default, with = || true)]
    lazy: bool,

    /// This prop should be deferred during a full reload, and sent back during a partial reload
    /// if it is requested. The defer logic is automatically implemented by the InertiaJS client
    /// for you, so if you're using a deferred prop it will automatically work.
    #[builder(default, with = || true)]
    defer: bool,

    /// This prop should always be sent back, regardless of if the request is partial or if the
    /// prop has been asked for. Only use this on data that is very cheap to calculate, or you
    /// may run into performance issues depending on how agressively you reload the page.
    #[builder(default, with = || true)]
    always: bool,

    /// This prop should be merged with the previous value of the prop set, if it exists. The merging
    /// logic itself is completely handled by the InertiaJS client, so all this really does is flag
    /// the prop to be merged but it will behave like a regular prop otherwise. This can be combined
    /// with other methods at-will.
    #[builder(default, with = || true)]
    merge: bool,
}

#[bon]
impl<F> Prop<F> {
    /// The `Prop` type itself specifis the conditions, set by the user, that
    /// the prop should be rendered under. The job of this method is to check those
    /// against the state of the request, and then produce an output that indicates
    /// what changes to apply to the extension.
    ///
    /// Effectively, lets say you have a defer prop. In here, we check the headers + component,
    /// see if the prop is requested as a part of the entire request, or if its a partial,
    /// and then either indicate to add the prop to the defer list, or to render it entirely.
    #[builder(finish_fn = build)]
    pub(crate) fn eval(
        self,
        #[builder(start_fn)] name: &'static str,
        req_headers: RequestHeaders<'_>,
        method: &Method,
        component: Option<&'static str>,
    ) -> EvalOutput<F> {
        let mut defer = false;
        let mut merge = false;

        let is_partial_request = req_headers.partial(method, component);

        // these are technically mutually exclusive, but we can be resilient to both
        let is_this_prop_requested = req_headers
            .partial_except()
            // for the except to match up, if specified, all props must not be our own name
            .map(|mut s| s.all(|p| p != name))
            .unwrap_or(true)
            // and if there is partial data, any of the partials must be our own name
            // if both of these are empty and its a full request, then it definitely is
            // requested.
            && req_headers
                .partial_data()
                .map(|mut s| s.any(|p| p == name))
                .unwrap_or(true);

        let prop = self.always
            || match (is_partial_request, is_this_prop_requested) {
                // if partial and requested, compute
                (true, true) => true,
                // if partial and not requested, don't compute but mark defer if this is a defer prop
                (true, false) => {
                    if self.defer {
                        defer = true;
                    }

                    false
                }
                // if its not partial and not requested don't compute
                (false, false) => false,
                // if its not partial and requested, compute if its not lazy
                (false, true) => !self.lazy,
            };

        if prop && self.merge {
            merge = true;
        }

        EvalOutput {
            prop_value: self.value,
            prop,
            lazy: self.lazy,
            defer,
            merge,
        }
    }
}

// allows a prop to be passed into `.prop` directly without being wrapped by any of these functions
// to indicate "normal" behavior of a prop
impl<F, P> From<F> for PropBuilder<F>
where
    F: AsyncFnOnce() -> P,
    P: PropControlFlow,
{
    fn from(f: F) -> Self {
        Prop::builder(f)
    }
}

/// This prop should only be rendered if explicitly requested in a partial reload, and be null otherwise.
///
/// This can only be used on props which return an `Option`.
pub fn lazy<F>(f: F) -> PropBuilder<F, prop_builder::SetLazy<prop_builder::Empty>> {
    Prop::builder(f).lazy()
}

/// This prop should be merged with the previous value of the prop set, if it exists. The merging
/// logic itself is completely handled by the InertiaJS client, so all this really does is flag
/// the prop to be merged but it will behave like a regular prop otherwise. This can be combined
/// with other methods at-will.
pub fn merge<F>(f: F) -> PropBuilder<F, prop_builder::SetMerge<prop_builder::Empty>> {
    Prop::builder(f).merge()
}

/// This prop should be deferred during a full reload, and sent back during a partial reload
/// if it is requested. The defer logic is automatically implemented by the InertiaJS client
/// for you, so if you're using a deferred prop it will automatically work.
pub fn defer<F>(f: F) -> PropBuilder<F, prop_builder::SetDefer<prop_builder::Empty>> {
    Prop::builder(f).defer()
}

/// This prop should always be sent back, regardless of if the request is partial or if the
/// prop has been asked for. Only use this on data that is very cheap to calculate, or you
/// may run into performance issues depending on how agressively you reload the page.
pub fn always<F>(f: F) -> PropBuilder<F, prop_builder::SetAlways<prop_builder::Empty>> {
    Prop::builder(f).always()
}

/// Since all props must be either a `Result` or an `Option`, this provides a utility to wrap
/// a non-result type as an infallible `Result`. This means it will never abort and hit the
/// error handling response middleware, nor will it become nullable
pub fn infallible<T>(t: T) -> Result<T, Infallible> {
    Ok(t)
}

/// Another helper type that simply omits a type. This is useful for if you have two different render branches,
/// and one must include something but one must not and avoids you having to turbofish.
pub fn null() -> Option<()> {
    None
}

/// The output of an eval that indicates which attributes should be
/// applied to the response extension.
#[derive(Debug)]
pub struct EvalOutput<F> {
    lazy: bool,
    prop_value: F,
    prop: bool,
    defer: bool,
    merge: bool,
}

impl<F> EvalOutput<F> {
    /// Given the eval output, apply the output to the extension
    pub async fn apply<P, Fut>(
        self,
        prop_name: &'static str,
        extension: &InertiaExtension,
    ) -> ControlFlow<P::Err>
    where
        F: FnOnce() -> Fut + Send,
        Fut: Future<Output = P> + Send,
        P: PropControlFlow,
    {
        if self.prop {
            let prop_control_flow = (self.prop_value)().await.into_control_flow();

            extension.add_prop(
                prop_name,
                serde_json::to_value(prop_control_flow?).expect("failed to serialize prop value"),
            );
        } else if self.lazy {
            extension.add_prop(prop_name, serde_json::Value::Null);
        }

        if self.merge {
            extension.merge_prop(prop_name);
        }

        if self.defer {
            extension.defer_prop(prop_name);
        }

        ControlFlow::Continue(())
    }
}

/// Implements how a prop should provide a value, or short circuit the request and return a different
/// response immediately. This is implemented just for `Option` and `Result` types, with result types
/// requiring that the `Err` variant be `IntoResponse`. This allows for most errors to be handled automatically
/// by us, here, and allows the user to write their own middleware that sits atop the driver that will be able
/// to intercept errors and render a different page if desired.
pub trait PropControlFlow {
    /// The successful value that will be serialized and added to the page response.
    type Value: Serialize;

    /// The error value that will immediately abort the rest of the props and produce an error page.
    type Err: IntoResponse;

    /// Wraps `self` as a control flow
    fn into_control_flow(self) -> ControlFlow<Self::Err, Self::Value>;
}

// Option control flow that will never short circuit the request, and will instead opt to
// make the option value act as itself and make the prop nullable
impl<T> PropControlFlow for Option<T>
where
    T: Serialize,
{
    type Value = Option<T>;
    type Err = Infallible;

    fn into_control_flow(self) -> ControlFlow<Self::Err, Self::Value> {
        ControlFlow::Continue(self)
    }
}

// Result control flow that short circuits into an error where the error becomes the entire
// response. Importantly, because this short circuits the request, the result type does not
// automatically make the value nullable, unless `T` itself is an option.
impl<T, E> PropControlFlow for Result<T, E>
where
    T: Serialize,
    E: IntoResponse,
{
    type Value = T;
    type Err = E;

    fn into_control_flow(self) -> ControlFlow<<Self as PropControlFlow>::Err, Self::Value> {
        match self {
            Ok(t) => ControlFlow::Continue(t),
            Err(e) => ControlFlow::Break(e),
        }
    }
}