quillmark-core 0.58.0

Core types and functionality for Quillmark
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use crate::{
    normalize::normalize_document,
    quill::{FieldSchema, FieldType},
    Diagnostic, ParsedDocument, Quill, QuillValue, RenderError, RenderOptions, RenderResult,
    Severity,
};

impl Quill {
    /// Attach a backend to this quill, returning a render-ready quill.
    pub fn with_backend(mut self, backend: Arc<dyn crate::Backend>) -> Self {
        self.resolved_backend = Some(backend);
        self
    }

    /// Return the resolved backend, if one has been attached.
    pub fn backend(&self) -> Option<&Arc<dyn crate::Backend>> {
        self.resolved_backend.as_ref()
    }

    /// Render a document to final artifacts.
    ///
    /// Note: page selection (`RenderOptions.pages`) is ignored in this one-shot
    /// convenience path. Use `open(...).render(...)` for page-selective rendering.
    pub fn render(
        &self,
        parsed: ParsedDocument,
        opts: &RenderOptions,
    ) -> Result<RenderResult, RenderError> {
        let all_pages_opts = RenderOptions {
            output_format: opts.output_format,
            ppi: opts.ppi,
            pages: None,
        };
        self.open(parsed)?.render(&all_pages_opts)
    }

    /// Open an iterative render session for this parsed document.
    pub fn open(&self, parsed: ParsedDocument) -> Result<crate::RenderSession, RenderError> {
        let backend = self.require_backend()?;
        let warning = self.ref_mismatch_warning(&parsed);
        let json_data = self.compile_data_internal(&parsed)?;
        let plate_content = self.plate.clone().unwrap_or_default();
        let session = backend.open(&plate_content, self, &json_data)?;
        Ok(session.with_warning(warning))
    }

    fn require_backend(&self) -> Result<&Arc<dyn crate::Backend>, RenderError> {
        self.resolved_backend.as_ref().ok_or_else(|| RenderError::NoBackend {
            diag: Box::new(
                Diagnostic::new(
                    Severity::Error,
                    format!(
                        "Quill '{}' has no backend attached; use engine.quill() or engine.quill_from_path() instead",
                        self.name
                    ),
                )
                .with_code("quill::no_backend".to_string())
                .with_hint(
                    "Create render-ready quills via engine.quill(tree) or engine.quill_from_path(path)".to_string(),
                ),
            ),
        })
    }

    fn ref_mismatch_warning(&self, parsed: &ParsedDocument) -> Option<Diagnostic> {
        let doc_ref = parsed.quill_reference().name.as_str();
        if doc_ref != self.name {
            Some(
                Diagnostic::new(
                    Severity::Warning,
                    format!(
                        "document declares QUILL '{}' but was rendered with '{}'",
                        doc_ref, self.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
        }
    }

    pub(crate) fn compile_data_internal(
        &self,
        parsed: &ParsedDocument,
    ) -> Result<serde_json::Value, RenderError> {
        let coerced_fields = self
            .config
            .coerce(parsed.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 fields and card values can be coerced to their declared types"
                                .to_string(),
                        ),
                ),
            })?;
        let parsed_coerced = ParsedDocument::new(coerced_fields, parsed.quill_reference().clone());
        self.validate_fields(&parsed_coerced)?;

        let normalized = normalize_document(parsed_coerced)?;
        let fields_with_defaults = self.apply_schema_defaults(normalized.fields());
        Ok(Self::fields_to_json(&fields_with_defaults))
    }

    fn validate_fields(&self, parsed: &ParsedDocument) -> Result<(), RenderError> {
        match self.config.validate(parsed.fields()) {
            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(),
                            ),
                    ),
                })
            }
        }
    }

    fn apply_schema_defaults(
        &self,
        fields: &HashMap<String, QuillValue>,
    ) -> HashMap<String, QuillValue> {
        let mut result = fields.clone();
        for (field_name, default_value) in self.config.defaults() {
            if !result.contains_key(&field_name) {
                result.insert(field_name, default_value);
            }
        }
        result
    }

    fn fields_to_json(fields: &HashMap<String, QuillValue>) -> serde_json::Value {
        let mut json_map = serde_json::Map::new();
        for (key, value) in fields {
            json_map.insert(key.clone(), value.as_json().clone());
        }
        serde_json::Value::Object(json_map)
    }

    pub fn build_transform_schema(&self) -> QuillValue {
        fn field_to_schema(field: &FieldSchema) -> serde_json::Value {
            let mut schema = serde_json::Map::new();
            match field.r#type {
                FieldType::String => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("string".to_string()),
                    );
                }
                FieldType::Markdown => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("string".to_string()),
                    );
                    schema.insert(
                        "contentMediaType".to_string(),
                        serde_json::Value::String("text/markdown".to_string()),
                    );
                }
                FieldType::Number => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("number".to_string()),
                    );
                }
                FieldType::Integer => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("integer".to_string()),
                    );
                }
                FieldType::Boolean => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("boolean".to_string()),
                    );
                }
                FieldType::Array => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("array".to_string()),
                    );
                    if let Some(items) = &field.items {
                        schema.insert("items".to_string(), field_to_schema(items));
                    }
                }
                FieldType::Object => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("object".to_string()),
                    );
                    if let Some(properties) = &field.properties {
                        let mut props = serde_json::Map::new();
                        for (name, prop) in properties {
                            props.insert(name.clone(), field_to_schema(prop));
                        }
                        schema.insert("properties".to_string(), serde_json::Value::Object(props));
                    }
                }
                FieldType::Date => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("string".to_string()),
                    );
                    schema.insert(
                        "format".to_string(),
                        serde_json::Value::String("date".to_string()),
                    );
                }
                FieldType::DateTime => {
                    schema.insert(
                        "type".to_string(),
                        serde_json::Value::String("string".to_string()),
                    );
                    schema.insert(
                        "format".to_string(),
                        serde_json::Value::String("date-time".to_string()),
                    );
                }
            }
            serde_json::Value::Object(schema)
        }

        let mut properties = serde_json::Map::new();
        for (name, field) in &self.config.main().fields {
            properties.insert(name.clone(), field_to_schema(field));
        }
        properties.insert(
            "BODY".to_string(),
            serde_json::json!({ "type": "string", "contentMediaType": "text/markdown" }),
        );

        let mut defs = serde_json::Map::new();
        for card in self.config.card_definitions() {
            let mut card_properties = serde_json::Map::new();
            for (name, field) in &card.fields {
                card_properties.insert(name.clone(), field_to_schema(field));
            }
            defs.insert(
                format!("{}_card", card.name),
                serde_json::json!({
                    "type": "object",
                    "properties": card_properties,
                }),
            );
        }

        QuillValue::from_json(serde_json::json!({
            "type": "object",
            "properties": properties,
            "$defs": defs,
        }))
    }
}