use std::collections::HashMap;
use crate::{DataSet, PreparedTemplate, PublishError, Result, SharedFonts};
#[cfg(feature = "html")]
use crate::{HtmlConfig, render_html_prepared};
#[cfg(feature = "pdf")]
use crate::{PdfConfig, render_pdf_prepared};
pub struct Renderer {
fonts: SharedFonts,
templates: HashMap<&'static str, PreparedTemplate>,
#[cfg(feature = "pdf")]
default_pdf_config: PdfConfig,
#[cfg(feature = "html")]
default_html_config: HtmlConfig,
#[cfg(feature = "tokio")]
limiter: Option<std::sync::Arc<tokio::sync::Semaphore>>,
#[cfg(feature = "tokio")]
deadline: Option<std::time::Duration>,
}
impl Renderer {
#[must_use]
pub fn new(fonts: SharedFonts) -> Self {
Self {
fonts,
templates: HashMap::new(),
#[cfg(feature = "pdf")]
default_pdf_config: PdfConfig::default(),
#[cfg(feature = "html")]
default_html_config: HtmlConfig::default(),
#[cfg(feature = "tokio")]
limiter: None,
#[cfg(feature = "tokio")]
deadline: None,
}
}
#[cfg(feature = "pdf")]
#[must_use]
pub fn with_default_pdf_config(mut self, config: PdfConfig) -> Self {
self.default_pdf_config = config;
self
}
#[cfg(feature = "html")]
#[must_use]
pub fn with_default_html_config(mut self, config: HtmlConfig) -> Self {
self.default_html_config = config;
self
}
#[cfg(feature = "tokio")]
#[must_use]
pub fn with_concurrency_limit(mut self, max_concurrent: usize) -> Self {
self.limiter = Some(std::sync::Arc::new(tokio::sync::Semaphore::new(
max_concurrent,
)));
self
}
#[cfg(feature = "tokio")]
#[must_use]
pub fn with_deadline(mut self, deadline: std::time::Duration) -> Self {
self.deadline = Some(deadline);
self
}
pub fn register_template(&mut self, key: &'static str, template: PreparedTemplate) {
self.templates.insert(key, template);
}
pub fn warm_up(&self) {
self.fonts.warm_up();
}
#[cfg(any(feature = "pdf", feature = "html"))]
fn lookup(&self, key: &str) -> Result<&PreparedTemplate> {
self.templates.get(key).ok_or_else(|| {
PublishError::InvalidTemplate(format!("no template registered for key '{key}'"))
})
}
#[cfg(feature = "pdf")]
pub fn render_pdf(&self, key: &str, data: DataSet) -> Result<Vec<u8>> {
let template = self.lookup(key)?;
render_pdf_prepared(&self.fonts, template, data, &self.default_pdf_config)
}
#[cfg(all(feature = "pdf", feature = "tokio"))]
pub async fn render_pdf_async(&self, key: &str, data: DataSet) -> Result<Vec<u8>> {
let template = self.lookup(key)?.clone();
let fonts = self.fonts.clone();
let config = self.default_pdf_config.clone();
self.run_blocking(move || render_pdf_prepared(&fonts, &template, data, &config))
.await
}
#[cfg(feature = "html")]
pub fn render_html(&self, key: &str, data: DataSet) -> Result<String> {
let template = self.lookup(key)?;
render_html_prepared(&self.fonts, template, data, &self.default_html_config)
}
#[cfg(all(feature = "html", feature = "tokio"))]
pub async fn render_html_async(&self, key: &str, data: DataSet) -> Result<String> {
let template = self.lookup(key)?.clone();
let fonts = self.fonts.clone();
let config = self.default_html_config.clone();
self.run_blocking(move || render_html_prepared(&fonts, &template, data, &config))
.await
}
#[cfg(feature = "tokio")]
async fn run_blocking<T, F>(&self, f: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce() -> Result<T> + Send + 'static,
{
let permit = match &self.limiter {
Some(sem) => Some(
sem.clone()
.acquire_owned()
.await
.map_err(|e| PublishError::InvalidTemplate(format!("limiter closed: {e}")))?,
),
None => None,
};
let join = tokio::task::spawn_blocking(move || {
let _permit = permit;
f()
});
let joined = match self.deadline {
Some(d) => tokio::time::timeout(d, join)
.await
.map_err(|_| PublishError::Timeout)?,
None => join.await,
};
joined.map_err(|e| PublishError::InvalidTemplate(format!("render task failed: {e}")))?
}
}
#[cfg(all(test, feature = "tokio", feature = "pdf"))]
mod tests {
use super::*;
use crate::{DataSet, TemplateSource};
fn renderer() -> Renderer {
let mut r = Renderer::new(SharedFonts::embedded_only()).with_concurrency_limit(2);
r.register_template(
"t",
PreparedTemplate::new(TemplateSource::InMemory {
content: "async facade test".to_owned(),
root: None,
})
.unwrap(),
);
r
}
#[test]
fn render_async_matches_sync() {
let r = renderer();
let sync = r.render_pdf("t", DataSet::new()).unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let asy = rt
.block_on(r.render_pdf_async("t", DataSet::new()))
.unwrap();
assert_eq!(&asy[0..5], b"%PDF-");
assert_eq!(sync, asy, "async and sync renders should match");
}
#[test]
fn render_async_with_generous_deadline_succeeds() {
let r = renderer().with_deadline(std::time::Duration::from_secs(30));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let bytes = rt
.block_on(r.render_pdf_async("t", DataSet::new()))
.unwrap();
assert_eq!(&bytes[0..5], b"%PDF-");
}
}