use std::str::FromStr;
use indexmap::IndexMap;
use super::{seed, CardSchema, Quill};
use crate::normalize::normalize_document;
use crate::quill::zero_value;
use crate::{
Card, Diagnostic, Document, Payload, QuillValue, RenderError, SeedOverlay, Severity, Version,
};
impl Quill {
pub fn compile_data(&self, doc: &Document) -> Result<serde_json::Value, RenderError> {
let coerced = self.coerce_and_validate(doc)?;
let normalized = normalize_document(coerced)?;
let config = self.config();
let main_resolved =
resolve_fields(&normalized.main().payload().to_index_map(), &config.main);
let cards_resolved: Vec<Card> = normalized
.cards()
.iter()
.map(|card| {
let fields = match config.card_kind(card.kind().unwrap_or("")) {
Some(schema) => resolve_fields(&card.payload().to_index_map(), schema),
None => card.payload().to_index_map(),
};
Card::from_parts(
rebuild_payload_with_meta(card, fields),
card.body().to_string(),
)
})
.collect();
let final_main = Card::from_parts(
rebuild_payload_with_meta(normalized.main(), main_resolved),
normalized.main().body().to_string(),
);
let final_doc = Document::from_main_and_cards(final_main, cards_resolved, Vec::new());
Ok(final_doc.to_plate_json())
}
pub fn dry_run(&self, doc: &Document) -> Result<(), RenderError> {
self.check_quill_reference(doc)?;
self.coerce_and_validate(doc).map(|_| ())
}
fn coerce_and_validate(&self, doc: &Document) -> Result<Document, RenderError> {
let config = self.config();
let coerced_payload = config
.coerce_payload(&doc.main().payload().to_index_map())
.map_err(coercion_error)?;
let mut coerced_cards: Vec<Card> = Vec::with_capacity(doc.cards().len());
for card in doc.cards() {
let coerced_fields = config
.coerce_card(card.kind().unwrap_or(""), &card.payload().to_index_map())
.map_err(coercion_error)?;
coerced_cards.push(Card::from_parts(
rebuild_payload_with_meta(card, coerced_fields),
card.body().to_string(),
));
}
let coerced_main = Card::from_parts(
rebuild_payload_with_meta(doc.main(), coerced_payload),
doc.main().body().to_string(),
);
let coerced_doc = Document::from_main_and_cards(coerced_main, coerced_cards, Vec::new());
self.validate_document(&coerced_doc)?;
Ok(coerced_doc)
}
pub fn check_quill_reference(&self, doc: &Document) -> Result<(), RenderError> {
let doc_ref = doc.quill_reference();
if doc_ref.name.as_str() != self.name() {
return Err(quill_mismatch(
format!(
"document declares $quill '{}' but was rendered with '{}'",
doc_ref,
self.name()
),
"quill::name_mismatch",
"render with the quill named by $quill, or update the $quill name",
));
}
let Ok(quill_version) = Version::from_str(&self.config().version) else {
return Ok(());
};
if !doc_ref.selector.matches(quill_version) {
return Err(quill_mismatch(
format!(
"document declares $quill '{}' but the loaded quill is version '{}'",
doc_ref, quill_version
),
"quill::version_mismatch",
"render with a quill whose version satisfies the selector, or update the $quill selector",
));
}
Ok(())
}
pub fn validate(&self, doc: &Document) -> Vec<Diagnostic> {
let mut diags = match self.config().validate_document(doc) {
Ok(()) => Vec::new(),
Err(errors) => errors.iter().map(|e| e.to_diagnostic()).collect(),
};
diags.extend(self.validate_seed(doc));
diags
}
fn validate_seed(&self, doc: &Document) -> Vec<Diagnostic> {
let Some(seed_map) = doc.main().payload().seed() else {
return Vec::new();
};
let config = self.config();
let mut diags = Vec::new();
for (kind, overlay) in seed_map {
let Some(card_schema) = config.card_kind(kind) else {
diags.push(
Diagnostic::new(
Severity::Warning,
format!("`$seed` overlay targets unknown card kind `{kind}`"),
)
.with_code("validation::seed_unknown_kind".to_string())
.with_path(format!("$seed.{kind}"))
.with_hint(format!(
"Remove the `{kind}` overlay, or rename it to a declared card kind."
)),
);
continue;
};
let Some(obj) = overlay.as_object() else {
diags.push(
Diagnostic::new(
Severity::Warning,
format!("`$seed.{kind}` must be a mapping of field overrides"),
)
.with_code("validation::seed_overlay_shape".to_string())
.with_path(format!("$seed.{kind}")),
);
continue;
};
for (field, value) in obj {
if field == "$body" {
continue;
}
let field_path = format!("$seed.{kind}.{field}");
let Some(field_schema) = card_schema.fields.get(field) else {
diags.push(
Diagnostic::new(
Severity::Warning,
format!("`$seed.{kind}.{field}` is not a field of card kind `{kind}`"),
)
.with_code("validation::seed_unknown_field".to_string())
.with_path(field_path),
);
continue;
};
let qv = QuillValue::from_json(value.clone());
for violation in
super::validation::validate_schema_literal(field_schema, &qv, &field_path)
{
diags.push(seed_violation_diagnostic(&violation));
}
}
}
diags
}
pub fn seed_document(&self) -> Document {
seed::seed_document(self)
}
pub fn seed_main(&self) -> Card {
seed::seed_main(self)
}
pub fn seed_card(&self, card_kind: &str, overlay: Option<&SeedOverlay>) -> Option<Card> {
seed::seed_card_for_kind(self, card_kind, overlay)
}
fn validate_document(&self, doc: &Document) -> Result<(), RenderError> {
match self.config().validate_document(doc) {
Ok(_) => Ok(()),
Err(errors) => {
let diags: Vec<Diagnostic> = errors
.iter()
.filter(|e| e.code() != "validation::field_absent")
.map(|e| e.to_diagnostic())
.collect();
if diags.is_empty() {
Ok(())
} else {
Err(RenderError::ValidationFailed { diags })
}
}
}
}
}
fn quill_mismatch(message: String, code: &str, hint: &str) -> RenderError {
RenderError::QuillMismatch {
diags: vec![Diagnostic::new(Severity::Error, message)
.with_code(code.to_string())
.with_hint(hint.to_string())],
}
}
fn seed_violation_diagnostic(v: &super::validation::ValidationError) -> Diagnostic {
let mut diag = Diagnostic::new(Severity::Warning, v.to_string())
.with_code(v.code().to_string())
.with_path(v.path().to_string());
if let Some(hint) = v.hint() {
diag = diag.with_hint(hint);
}
diag
}
fn coercion_error(e: impl std::fmt::Display) -> RenderError {
RenderError::ValidationFailed {
diags: vec![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())],
}
}
fn resolve_fields(
fields: &IndexMap<String, QuillValue>,
schema: &CardSchema,
) -> IndexMap<String, QuillValue> {
let mut result = fields.clone();
for (name, field) in &schema.fields {
if result.contains_key(name) {
continue;
}
let value = field.default.clone().unwrap_or_else(|| zero_value(field));
result.insert(name.clone(), value);
}
result
}
fn rebuild_payload_with_meta(source: &Card, fields: IndexMap<String, QuillValue>) -> Payload {
let mut payload = Payload::from_index_map(fields);
if let Some(q) = source.quill() {
payload.set_quill(q.clone());
}
if let Some(k) = source.kind() {
payload.set_kind(k.to_string());
}
if let Some(id) = source.id() {
payload.set_id(id.to_string());
}
payload
}