use std::path::Path;
use serde::{Serialize, Deserialize};
use crate::error::{PapermakeError, Result};
use crate::schema::Schema;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TemplateId(pub String);
impl From<String> for TemplateId {
fn from(s: String) -> Self {
TemplateId(s)
}
}
impl From<&str> for TemplateId {
fn from(s: &str) -> Self {
TemplateId(s.to_string())
}
}
impl AsRef<str> for TemplateId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Template {
pub id: TemplateId,
pub name: String,
pub content: String,
pub schema: Schema,
pub description: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
}
impl Template {
pub fn new(id: impl Into<TemplateId>, name: impl Into<String>, content: impl Into<String>, schema: Schema) -> Self {
let now = time::OffsetDateTime::now_utc();
Template {
id: id.into(),
name: name.into(),
content: content.into(),
schema,
description: None,
created_at: now,
updated_at: now,
}
}
pub fn builder(id: impl Into<TemplateId>) -> TemplateBuilder {
TemplateBuilder::new(id.into())
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn validate_data(&self, data: &serde_json::Value) -> Result<()> {
self.schema.validate(data)
}
pub fn render(&self, data: &serde_json::Value) -> Result<crate::render::RenderResult> {
crate::render::render_pdf(self, data, None)
}
pub fn render_with_options(&self, data: &serde_json::Value, options: crate::render::RenderOptions) -> Result<crate::render::RenderResult> {
crate::render::render_pdf(self, data, Some(options))
}
pub fn render_with_cache(&self, data: &serde_json::Value, world_cache: Option<&mut crate::typst::TypstWorld>) -> Result<crate::render::RenderResult> {
crate::render::render_pdf_with_cache(self, data, world_cache, None)
}
pub fn render_with_cache_and_options(&self, data: &serde_json::Value, world_cache: Option<&mut crate::typst::TypstWorld>, options: crate::render::RenderOptions) -> Result<crate::render::RenderResult> {
crate::render::render_pdf_with_cache(self, data, world_cache, Some(options))
}
pub fn from_file_content(id: impl Into<TemplateId>, content: &str) -> Result<Self> {
let parts: Vec<&str> = content.split("---").collect();
if parts.len() < 3 {
return Err(PapermakeError::Template(
"Invalid template format. Expected frontmatter between '---' markers.".to_string()
));
}
let template_content = parts[2].trim().to_string();
let schema = Schema::default();
Ok(Template {
id: id.into(),
name: "".to_string(), content: template_content,
schema,
description: None,
created_at: time::OffsetDateTime::now_utc(),
updated_at: time::OffsetDateTime::now_utc(),
})
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| PapermakeError::Io(e))?;
let id = path.as_ref().file_stem().unwrap().to_string_lossy().to_string();
Self::from_file_content(id, &content)
}
}
#[derive(Debug)]
pub struct TemplateBuilder {
id: TemplateId,
name: Option<String>,
content: Option<String>,
schema: Option<Schema>,
description: Option<String>,
}
impl TemplateBuilder {
pub fn new(id: TemplateId) -> Self {
TemplateBuilder {
id,
name: None,
content: None,
schema: None,
description: None,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn content_from_file(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
let content = std::fs::read_to_string(path)
.map_err(|e| PapermakeError::Io(e))?;
self.content = Some(content);
Ok(self)
}
pub fn schema(mut self, schema: Schema) -> Self {
self.schema = Some(schema);
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn build(self) -> Result<Template> {
let name = self.name.ok_or_else(|| PapermakeError::Template("Template name is required".to_string()))?;
let content = self.content.ok_or_else(|| PapermakeError::Template("Template content is required".to_string()))?;
let schema = self.schema.unwrap_or_default();
let now = time::OffsetDateTime::now_utc();
Ok(Template {
id: self.id,
name,
content,
schema,
description: self.description,
created_at: now,
updated_at: now,
})
}
pub fn build_cached(self) -> Result<crate::cache::CachedTemplate> {
use crate::cache::TemplateCache;
Ok(self.build()?.with_cache())
}
pub fn from_raw_content_cached(id: impl Into<TemplateId>, content: impl Into<String>) -> Result<crate::cache::CachedTemplate> {
use crate::cache::TemplateCache;
let template = Self::new(id.into())
.name("Generated Template") .content(content)
.build()?;
Ok(template.with_cache())
}
}