haven 0.1.2

Actix + React + Vite integration for server-rendered applications
Documentation
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;

use serde::Serialize;
use serde_json::Value;
use tokio_stream::wrappers::UnboundedReceiverStream;
use uuid::Uuid;

use crate::HeadTag;
use crate::RendererError;
use crate::http_protocol::{PartialReloadKind, PartialReloadRequest};
use crate::protocol::{PageData, PageEnvelope, PageMeta, RenderRequest};
use crate::renderer::{
    DirectRender, DirectStream, PageRoute, RenderedPage, RenderedPageStream, RendererMode,
    ResolvedRuntimeRendererConfig, RuntimeRendererConfig,
};
use crate::runtime::LocalRuntime;

#[derive(Debug, Clone)]
pub struct RendererConfig {
    pub mode: RendererMode,
    pub js_entrypoint: PathBuf,
    pub js_source_dir: PathBuf,
    pub client_manifest_path: Option<PathBuf>,
    pub project_root: Option<PathBuf>,
    pub translations_dir: Option<PathBuf>,
    pub default_locale: String,
    pub fallback_locale: String,
    pub vite_dev_server_origin: Option<String>,
    pub start_vite_dev_server: bool,
    pub version: String,
    pub url: Option<String>,
    pub asset_script_url: Option<String>,
    pub title: String,
}

impl RendererConfig {
    fn normalize(self) -> Result<ResolvedRuntimeRendererConfig, RendererError> {
        RuntimeRendererConfig {
            mode: self.mode,
            js_entrypoint: self.js_entrypoint,
            js_source_dir: self.js_source_dir,
            client_manifest_path: self.client_manifest_path,
            project_root: self.project_root,
            translations_dir: self.translations_dir,
            default_locale: self.default_locale,
            fallback_locale: self.fallback_locale,
            vite_dev_server_origin: self.vite_dev_server_origin,
            start_vite_dev_server: self.start_vite_dev_server,
            version: self.version,
            url: self.url,
            asset_script_url: self.asset_script_url,
            title: self.title,
        }
        .normalize()
    }

    pub fn development() -> Self {
        Self::default()
    }
}

impl Default for RendererConfig {
    fn default() -> Self {
        let config = RuntimeRendererConfig::development();
        Self {
            mode: config.mode,
            js_entrypoint: config.js_entrypoint,
            js_source_dir: config.js_source_dir,
            client_manifest_path: config.client_manifest_path,
            project_root: config.project_root,
            translations_dir: config.translations_dir,
            default_locale: config.default_locale,
            fallback_locale: config.fallback_locale,
            vite_dev_server_origin: config.vite_dev_server_origin,
            start_vite_dev_server: config.start_vite_dev_server,
            version: config.version,
            url: config.url,
            asset_script_url: config.asset_script_url,
            title: config.title,
        }
    }
}

#[derive(Clone)]
pub struct RendererState {
    inner: Arc<RendererStateInner>,
}

struct RendererStateInner {
    runtime: LocalRuntime,
    version: String,
    url: String,
    title: String,
    locale: String,
}

#[derive(Clone)]
pub struct RenderBuilder<M = DirectRender> {
    renderer: RendererState,
    route: PageRoute,
    _mode: M,
}

pub type StreamingRenderBuilder = RenderBuilder<DirectStream>;

impl RendererState {
    pub fn new(config: RendererConfig) -> Result<Self, RendererError> {
        let resolved = config.normalize()?;
        let runtime = LocalRuntime::new(resolved.clone())?;

        Ok(Self {
            inner: Arc::new(RendererStateInner {
                runtime,
                version: resolved.version,
                url: resolved.url.unwrap_or_else(|| "/".into()),
                title: resolved.title,
                locale: resolved.default_locale,
            }),
        })
    }

    pub fn render(&self, page: impl Into<String>) -> RenderBuilder {
        RenderBuilder::new(
            self.clone(),
            self.route(page.into(), self.inner.url.clone()),
            DirectRender,
        )
    }

    pub fn stream(&self, page: impl Into<String>) -> StreamingRenderBuilder {
        RenderBuilder::new(
            self.clone(),
            self.route(page.into(), self.inner.url.clone()),
            DirectStream,
        )
    }

    pub fn route(&self, component: impl Into<String>, url: impl Into<String>) -> PageRoute {
        PageRoute {
            page: self.base_page_data_with_url(component.into(), url.into()),
            meta: Self::empty_page_meta(),
        }
    }

    pub fn page_envelope(&self, page: PageData) -> PageEnvelope {
        self.page_envelope_with_meta(page, Self::empty_page_meta())
    }

    pub fn page_envelope_with_resources(&self, page: PageData, resources: Value) -> PageEnvelope {
        self.page_envelope_with_meta(
            page,
            PageMeta {
                title: None,
                resources,
                head: vec![],
            },
        )
    }

    pub fn page_envelope_with_meta(&self, page: PageData, meta: PageMeta) -> PageEnvelope {
        RenderRequest {
            id: Uuid::new_v4().to_string(),
            page,
            meta,
            errors: empty_json_object(),
        }
    }

    pub async fn render_page(&self, page: PageData) -> Result<RenderedPage, RendererError> {
        let response = self.inner.runtime.render(&self.page_envelope(page)).await?;
        Ok(Self::rendered_page_from_response(response))
    }

    pub async fn stream_page(&self, page: PageData) -> Result<RenderedPageStream, RendererError> {
        self.stream_envelope(self.page_envelope(page)).await
    }

    pub async fn render_route(&self, route: PageRoute) -> Result<RenderedPage, RendererError> {
        self.render_envelope(self.page_envelope_with_meta(route.page, route.meta))
            .await
    }

    pub async fn stream_route(
        &self,
        route: PageRoute,
    ) -> Result<RenderedPageStream, RendererError> {
        self.stream_envelope(self.page_envelope_with_meta(route.page, route.meta))
            .await
    }

    pub async fn render_envelope(
        &self,
        envelope: PageEnvelope,
    ) -> Result<RenderedPage, RendererError> {
        let response = self.inner.runtime.render(&envelope).await?;
        Ok(Self::rendered_page_from_response(response))
    }

    pub async fn stream_envelope(
        &self,
        envelope: PageEnvelope,
    ) -> Result<RenderedPageStream, RendererError> {
        let (stream, status) = self.inner.runtime.render_stream(&envelope).await?;
        Ok(RenderedPageStream {
            stream: UnboundedReceiverStream::new(stream),
            status,
        })
    }

    pub async fn reload(&self) -> Result<(), RendererError> {
        self.inner.runtime.reload().await
    }

    pub fn default_locale(&self) -> &str {
        &self.inner.locale
    }

    pub fn default_url(&self) -> &str {
        &self.inner.url
    }

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

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

    pub fn partial_page_envelope(
        &self,
        route: &PageRoute,
        partial: PartialReloadRequest,
    ) -> crate::PartialPageEnvelope {
        let props = top_level_values(
            route.page.props.as_object(),
            &partial.entries,
            PartialReloadKind::Props,
        );
        let resources = top_level_values(
            route.meta.resources.as_object(),
            &partial.entries,
            PartialReloadKind::Resources,
        );

        crate::PartialPageEnvelope {
            component: route.page.component.clone(),
            props,
            resources,
            errors: None,
            url: route.page.url.clone(),
            version: route.page.version.clone(),
            locale: route.page.locale.clone(),
            title: route.meta.title.clone(),
        }
    }

    fn base_page_data_with_url(&self, component: String, url: String) -> PageData {
        PageData {
            component,
            props: empty_json_object(),
            url,
            version: self.inner.version.clone(),
            locale: self.inner.locale.clone(),
        }
    }

    fn empty_page_meta() -> PageMeta {
        PageMeta {
            title: None,
            resources: empty_json_object(),
            head: vec![],
        }
    }

    fn rendered_page_from_response(response: crate::RenderResponse) -> RenderedPage {
        RenderedPage {
            html: response.html,
            head: (!response.head.is_empty()).then(|| response.head.join("\n")),
            status: response.status,
        }
    }
}

impl<M> RenderBuilder<M> {
    fn new(renderer: RendererState, route: PageRoute, mode: M) -> Self {
        Self {
            renderer,
            route,
            _mode: mode,
        }
    }

    pub fn url(mut self, url: impl Into<String>) -> Self {
        self.route.page.url = url.into();
        self
    }

    pub fn props<T>(mut self, props: T) -> Result<Self, RendererError>
    where
        T: Serialize,
    {
        self.route = self.route.props(props)?;
        Ok(self)
    }

    pub fn locale(mut self, locale: impl Into<String>) -> Self {
        self.route = self.route.locale(locale);
        self
    }

    pub fn version(mut self, version: impl Into<String>) -> Self {
        self.route = self.route.version(version);
        self
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.route = self.route.title(title);
        self
    }

    pub fn resources<T>(mut self, resources: T) -> Result<Self, RendererError>
    where
        T: Serialize,
    {
        self.route = self.route.resources(resources)?;
        Ok(self)
    }

    pub fn context<T>(mut self, context: T) -> Result<Self, RendererError>
    where
        T: Serialize,
    {
        self.route = self.route.context(context)?;
        Ok(self)
    }

    pub fn head(mut self, head: Vec<HeadTag>) -> Self {
        self.route = self.route.head(head);
        self
    }

    pub fn with_page(mut self, page: PageData) -> Self {
        self.route = self.route.with_page(page);
        self
    }
}

impl std::future::IntoFuture for RenderBuilder {
    type Output = Result<RenderedPage, RendererError>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move { self.renderer.render_route(self.route).await })
    }
}

impl std::future::IntoFuture for RenderBuilder<DirectStream> {
    type Output = Result<RenderedPageStream, RendererError>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move { self.renderer.stream_route(self.route).await })
    }
}

fn empty_json_object() -> Value {
    Value::Object(Default::default())
}

fn top_level_values(
    source: Option<&serde_json::Map<String, Value>>,
    entries: &[crate::PartialReloadEntry],
    kind: PartialReloadKind,
) -> serde_json::Map<String, Value> {
    let mut values = serde_json::Map::new();

    for entry in entries {
        if entry.kind != kind {
            continue;
        }

        let value = source
            .and_then(|map| map.get(&entry.key))
            .cloned()
            .unwrap_or(Value::Null);
        values.insert(entry.key.clone(), value);
    }

    values
}