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
}