use std::fmt;
use std::sync::Arc;
use super::error::InertiaError;
use super::props::SharedProps;
use crate::http::security::CspNonce;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AssetVersion(Arc<str>);
impl AssetVersion {
pub fn new(version: impl AsRef<str>) -> Result<Self, InertiaError> {
let version = version.as_ref();
axum::http::HeaderValue::from_str(version)
.map_err(axum::http::Error::from)
.map_err(InertiaError::Header)?;
Ok(AssetVersion(Arc::from(version)))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for AssetVersion {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, Clone)]
pub struct ScriptBody {
html: Arc<str>,
nonce: Option<CspNonce>,
}
impl ScriptBody {
pub(crate) fn from_escaped(html: Arc<str>, nonce: Option<CspNonce>) -> ScriptBody {
ScriptBody { html, nonce }
}
#[must_use]
pub fn nonce(&self) -> Option<&CspNonce> {
self.nonce.as_ref()
}
#[must_use]
pub fn nonce_attribute(&self) -> String {
self.nonce
.as_ref()
.map(CspNonce::attribute)
.unwrap_or_default()
}
}
impl fmt::Display for ScriptBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.html)
}
}
pub trait RootDocument: Send + Sync {
fn render(&self, body: ScriptBody) -> String;
}
impl<T> RootDocument for T
where
T: Fn(ScriptBody) -> String + Send + Sync,
{
fn render(&self, body: ScriptBody) -> String {
self(body)
}
}
#[derive(Clone)]
pub struct InertiaConfig {
inner: Arc<ConfigInner>,
}
impl fmt::Debug for InertiaConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InertiaConfig")
.field("version", &self.inner.version)
.field("shared_props", &self.inner.shared_props.is_empty())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
struct ConfigInner {
version: Option<AssetVersion>,
root_document: Arc<dyn RootDocument>,
shared_props: SharedProps,
page_id: String,
}
impl InertiaConfig {
pub fn new(
version: impl AsRef<str>,
root_document: impl RootDocument + 'static,
) -> Result<InertiaConfig, InertiaError> {
Ok(InertiaConfig {
inner: Arc::new(ConfigInner {
version: Some(AssetVersion::new(version)?),
root_document: Arc::new(root_document),
shared_props: SharedProps::new(),
page_id: "app".to_string(),
}),
})
}
pub fn versionless(root_document: impl RootDocument + 'static) -> InertiaConfig {
InertiaConfig {
inner: Arc::new(ConfigInner {
version: None,
root_document: Arc::new(root_document),
shared_props: SharedProps::new(),
page_id: "app".to_string(),
}),
}
}
pub fn with_shared(mut self, shared: SharedProps) -> Self {
Arc::make_mut(&mut self.inner).shared_props = shared;
self
}
pub fn with_page_id(mut self, id: impl Into<String>) -> Self {
Arc::make_mut(&mut self.inner).page_id = id.into();
self
}
pub fn version(&self) -> Option<&AssetVersion> {
self.inner.version.as_ref()
}
pub(crate) fn version_str(&self) -> &str {
self.inner.version.as_ref().map_or("", AssetVersion::as_str)
}
pub(crate) fn root_document(&self) -> &Arc<dyn RootDocument> {
&self.inner.root_document
}
pub(crate) fn shared_props(&self) -> &SharedProps {
&self.inner.shared_props
}
pub(crate) fn page_id(&self) -> &str {
&self.inner.page_id
}
}
pub fn default_root_document(title: &str) -> impl RootDocument + use<> {
let title = title.to_string();
move |body: ScriptBody| {
let nonce = body.nonce_attribute();
format!(
"<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n \
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n \
<title>{title}</title>\n <link{nonce} rel=\"stylesheet\" href=\"/css/app.css\" />\n</head>\n\
<body>\n {body}\n <script{nonce} type=\"module\" src=\"/js/app.js\"></script>\n</body>\n</html>"
)
}
}
pub fn vite_root_document(
title: &str,
assets: &crate::assets::Assets,
entry: &str,
) -> impl RootDocument + use<> {
let title = title.to_string();
let resolved = assets.resolve(entry);
let dev = assets.is_dev();
move |body: ScriptBody| {
let nonce = body.nonce().map(CspNonce::as_str);
let head = crate::assets::style_tags(
resolved.as_ref().map(|r| r.css.as_slice()).unwrap_or(&[]),
nonce,
);
let scripts =
crate::assets::script_tags(resolved.as_ref().map(|r| r.js.as_str()), dev, nonce);
format!(
"<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n \
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n \
<title>{title}</title>\n {head}\n</head>\n\
<body>\n {body}\n {scripts}\n</body>\n</html>"
)
}
}