use indexmap::IndexMap;
use std::sync::Arc;
use quillmark_core::{
normalize::normalize_document, Backend, Card, Diagnostic, Document, OutputFormat, QuillSource,
QuillValue, RenderError, RenderOptions, RenderResult, RenderSession, Severity,
};
#[derive(Clone)]
pub struct Quill {
source: Arc<QuillSource>,
backend: Arc<dyn Backend>,
}
struct PreparedRenderContext {
json_data: serde_json::Value,
plate_content: String,
}
impl Quill {
pub(crate) fn new(source: Arc<QuillSource>, backend: Arc<dyn Backend>) -> Self {
Self { source, backend }
}
pub fn source(&self) -> &QuillSource {
&self.source
}
pub fn backend_id(&self) -> &str {
self.backend.id()
}
pub fn supported_formats(&self) -> &'static [OutputFormat] {
self.backend.supported_formats()
}
pub fn name(&self) -> &str {
self.source.name()
}
pub fn render(
&self,
doc: &Document,
opts: &RenderOptions,
) -> Result<RenderResult, RenderError> {
let session = self.open(doc)?;
let resolved = self.resolve_options(opts);
session.render(&resolved)
}
pub fn open(&self, doc: &Document) -> Result<RenderSession, RenderError> {
let context = self.prepare_render_context(doc)?;
let warning = self.ref_mismatch_warning(doc);
let session =
self.backend
.open(&context.plate_content, &self.source, &context.json_data)?;
Ok(session.with_warning(warning))
}
fn resolve_options(&self, opts: &RenderOptions) -> RenderOptions {
let output_format = opts
.output_format
.or_else(|| self.backend.supported_formats().first().copied());
RenderOptions {
output_format,
ppi: opts.ppi,
pages: opts.pages.clone(),
}
}
pub fn compile_data(&self, doc: &Document) -> Result<serde_json::Value, RenderError> {
let coerced_frontmatter = self
.source
.config()
.coerce_frontmatter(doc.frontmatter())
.map_err(|e| RenderError::ValidationFailed {
diag: Box::new(
Diagnostic::new(Severity::Error, e.to_string())
.with_code("validation::coercion_failed".to_string())
.with_hint(
"Ensure all fields can be coerced to their declared types".to_string(),
),
),
})?;
let mut coerced_cards: Vec<Card> = Vec::new();
for card in doc.cards() {
let coerced_fields = self
.source
.config()
.coerce_card(card.tag(), card.fields())
.map_err(|e| RenderError::ValidationFailed {
diag: Box::new(
Diagnostic::new(Severity::Error, e.to_string())
.with_code("validation::coercion_failed".to_string())
.with_hint(
"Ensure all card fields can be coerced to their declared types"
.to_string(),
),
),
})?;
coerced_cards.push(Card::new_internal(
card.tag().to_string(),
coerced_fields,
card.body().to_string(),
));
}
let coerced_doc = Document::new_internal(
doc.quill_reference().clone(),
coerced_frontmatter,
doc.body().to_string(),
coerced_cards,
doc.warnings().to_vec(),
);
self.validate_document(&coerced_doc)?;
let normalized = normalize_document(coerced_doc)?;
let frontmatter_with_defaults = self.apply_frontmatter_defaults(normalized.frontmatter());
let cards_with_defaults: Vec<Card> = normalized
.cards()
.iter()
.map(|card| {
let fields_with_defaults = self.apply_card_defaults(card.tag(), card.fields());
Card::new_internal(
card.tag().to_string(),
fields_with_defaults,
card.body().to_string(),
)
})
.collect();
let final_doc = Document::new_internal(
normalized.quill_reference().clone(),
frontmatter_with_defaults,
normalized.body().to_string(),
cards_with_defaults,
normalized.warnings().to_vec(),
);
Ok(final_doc.to_plate_json())
}
fn prepare_render_context(&self, doc: &Document) -> Result<PreparedRenderContext, RenderError> {
Ok(PreparedRenderContext {
json_data: self.compile_data(doc)?,
plate_content: self.plate_content().unwrap_or_default(),
})
}
fn ref_mismatch_warning(&self, doc: &Document) -> Option<Diagnostic> {
let doc_ref = doc.quill_reference().name.as_str();
if doc_ref != self.source.name() {
Some(
Diagnostic::new(
Severity::Warning,
format!(
"document declares QUILL '{}' but was rendered with '{}'",
doc_ref,
self.source.name()
),
)
.with_code("quill::ref_mismatch".to_string())
.with_hint(
"the QUILL field is informational; ensure you are rendering with the intended quill"
.to_string(),
),
)
} else {
None
}
}
fn apply_frontmatter_defaults(
&self,
frontmatter: &IndexMap<String, QuillValue>,
) -> IndexMap<String, QuillValue> {
let mut result = frontmatter.clone();
for (field_name, default_value) in self.source.config().defaults() {
if !result.contains_key(&field_name) {
result.insert(field_name, default_value);
}
}
result
}
fn apply_card_defaults(
&self,
card_tag: &str,
fields: &IndexMap<String, QuillValue>,
) -> IndexMap<String, QuillValue> {
let mut result = fields.clone();
if let Some(card_defaults) = self.source.config().card_defaults(card_tag) {
for (field_name, default_value) in card_defaults {
if !result.contains_key(&field_name) {
result.insert(field_name, default_value);
}
}
}
result
}
fn plate_content(&self) -> Option<String> {
self.source
.plate()
.filter(|s| !s.is_empty())
.map(str::to_string)
}
pub fn dry_run(&self, doc: &Document) -> Result<(), RenderError> {
let coerced_frontmatter = self
.source
.config()
.coerce_frontmatter(doc.frontmatter())
.map_err(|e| RenderError::ValidationFailed {
diag: Box::new(
Diagnostic::new(Severity::Error, e.to_string())
.with_code("validation::coercion_failed".to_string())
.with_hint(
"Ensure all fields and card values can be coerced to their declared types"
.to_string(),
),
),
})?;
let mut coerced_cards: Vec<Card> = Vec::new();
for card in doc.cards() {
let coerced_fields = self
.source
.config()
.coerce_card(card.tag(), card.fields())
.map_err(|e| RenderError::ValidationFailed {
diag: Box::new(
Diagnostic::new(Severity::Error, e.to_string())
.with_code("validation::coercion_failed".to_string())
.with_hint(
"Ensure all card fields can be coerced to their declared types"
.to_string(),
),
),
})?;
coerced_cards.push(Card::new_internal(
card.tag().to_string(),
coerced_fields,
card.body().to_string(),
));
}
let coerced_doc = Document::new_internal(
doc.quill_reference().clone(),
coerced_frontmatter,
doc.body().to_string(),
coerced_cards,
doc.warnings().to_vec(),
);
self.validate_document(&coerced_doc)?;
Ok(())
}
fn validate_document(&self, doc: &Document) -> Result<(), RenderError> {
match self.source.config().validate_document(doc) {
Ok(_) => Ok(()),
Err(errors) => {
let error_message = errors
.into_iter()
.map(|e| format!("- {}", e))
.collect::<Vec<_>>()
.join("\n");
Err(RenderError::ValidationFailed {
diag: Box::new(
Diagnostic::new(Severity::Error, error_message)
.with_code("validation::document_invalid".to_string())
.with_hint(
"Ensure all required fields are present and have correct types"
.to_string(),
),
),
})
}
}
}
}
impl std::fmt::Debug for Quill {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Quill")
.field("name", &self.source.name())
.field("backend", &self.backend.id())
.finish()
}
}