use std::collections::HashMap;
use quillmark_core::quill::FileTreeNode;
use quillmark_core::Document;
use crate::{Quill, Quillmark};
use super::{project_form, FormFieldSource, FormProjection};
fn quill_from_yaml(yaml: &str) -> Quill {
let mut files = HashMap::new();
files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: yaml.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files };
Quillmark::new()
.quill(root)
.expect("quill_from_yaml: engine.quill failed")
}
#[test]
fn project_form_all_fields_present() {
let quill = quill_from_yaml(
r#"
Quill:
name: form_test
version: "1.0"
backend: typst
description: Form projection test
main:
fields:
title:
type: string
status:
type: string
default: draft
"#,
);
let md = "---\nQUILL: form_test\ntitle: \"My Title\"\nstatus: \"final\"\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
assert!(proj.diagnostics.is_empty(), "no diagnostics expected");
assert!(proj.cards.is_empty(), "no cards expected");
let title_fv = proj.main.values.get("title").expect("title field");
assert_eq!(title_fv.source, FormFieldSource::Document);
assert_eq!(
title_fv.value.as_ref().and_then(|v| v.as_str()),
Some("My Title")
);
let status_fv = proj.main.values.get("status").expect("status field");
assert_eq!(status_fv.source, FormFieldSource::Document);
assert_eq!(
status_fv.value.as_ref().and_then(|v| v.as_str()),
Some("final")
);
assert_eq!(
status_fv.default.as_ref().and_then(|v| v.as_str()),
Some("draft")
);
}
#[test]
fn project_form_missing_field_uses_default() {
let quill = quill_from_yaml(
r#"
Quill:
name: form_defaults_test
version: "1.0"
backend: typst
description: Missing fields use defaults
main:
fields:
title:
type: string
required: true
status:
type: string
default: draft
notes:
type: string
"#,
);
let md = "---\nQUILL: form_defaults_test\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
assert!(
proj.diagnostics.iter().any(|d| d.message.contains("title")),
"expected validation diagnostic for required 'title'; got: {:?}",
proj.diagnostics
);
let status_fv = proj.main.values.get("status").expect("status field");
assert_eq!(status_fv.source, FormFieldSource::Default);
assert!(
status_fv.value.is_none(),
"value should be None when not in document"
);
assert_eq!(
status_fv.default.as_ref().and_then(|v| v.as_str()),
Some("draft")
);
let notes_fv = proj.main.values.get("notes").expect("notes field");
assert_eq!(notes_fv.source, FormFieldSource::Missing);
assert!(notes_fv.value.is_none());
assert!(notes_fv.default.is_none());
}
#[test]
fn project_form_unknown_card_tag_drops_card_and_emits_diagnostic() {
let quill = quill_from_yaml(
r#"
Quill:
name: unknown_card_test
version: "1.0"
backend: typst
description: Unknown card tag test
main:
fields:
title:
type: string
cards:
known_card:
fields:
note:
type: string
"#,
);
let md = "---\nQUILL: unknown_card_test\ntitle: \"T\"\n---\n\n\
---\nCARD: known_card\nnote: \"A\"\n---\n\n\
---\nCARD: ghost_card\nnote: \"B\"\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
assert_eq!(proj.cards.len(), 1, "only known_card should be projected");
assert_eq!(proj.cards[0].schema.name, "known_card");
let unknown_diag = proj
.diagnostics
.iter()
.find(|d| d.code.as_deref() == Some("form::unknown_card_tag"))
.expect("expected unknown_card_tag diagnostic");
assert!(
unknown_diag.message.contains("ghost_card"),
"diagnostic should name the tag: {:?}",
unknown_diag.message
);
}
#[test]
fn project_form_card_field_sources() {
let quill = quill_from_yaml(
r#"
Quill:
name: card_fields_test
version: "1.0"
backend: typst
description: Card field source test
main:
fields:
title:
type: string
cards:
indorsement:
fields:
signature_block:
type: string
required: true
office:
type: string
default: HQ
extra:
type: string
"#,
);
let md = "---\nQUILL: card_fields_test\ntitle: \"T\"\n---\n\n\
---\nCARD: indorsement\nsignature_block: \"Col Smith\"\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
assert_eq!(proj.cards.len(), 1);
let card = &proj.cards[0];
let sig = card.values.get("signature_block").expect("signature_block");
assert_eq!(sig.source, FormFieldSource::Document);
assert_eq!(
sig.value.as_ref().and_then(|v| v.as_str()),
Some("Col Smith")
);
let office = card.values.get("office").expect("office");
assert_eq!(office.source, FormFieldSource::Default);
assert!(office.value.is_none());
assert_eq!(office.default.as_ref().and_then(|v| v.as_str()), Some("HQ"));
let extra = card.values.get("extra").expect("extra");
assert_eq!(extra.source, FormFieldSource::Missing);
assert!(extra.value.is_none());
assert!(extra.default.is_none());
}
#[test]
fn project_form_validation_diagnostics_appear() {
let quill = quill_from_yaml(
r#"
Quill:
name: validation_diag_test
version: "1.0"
backend: typst
description: Validation diagnostics test
main:
fields:
count:
type: integer
required: true
"#,
);
let md = "---\nQUILL: validation_diag_test\ncount: \"not-a-number\"\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
let val_diag = proj
.diagnostics
.iter()
.find(|d| d.code.as_deref() == Some("form::validation_error"))
.expect("expected a validation diagnostic");
assert!(
val_diag.message.contains("count"),
"diagnostic should mention field name; got: {:?}",
val_diag.message
);
}
#[test]
fn project_form_serializes_cleanly() {
let quill = quill_from_yaml(
r#"
Quill:
name: serial_test
version: "1.0"
backend: typst
description: Serialization smoke test
main:
fields:
title:
type: string
default: Untitled
count:
type: integer
"#,
);
let md = "---\nQUILL: serial_test\ntitle: \"Hello\"\n---\n";
let doc = Document::from_markdown(md).unwrap();
let proj = project_form(&quill, &doc);
let json = serde_json::to_string(&proj).expect("FormProjection must serialize");
let back: FormProjection =
serde_json::from_str(&json).expect("FormProjection must deserialize");
assert_eq!(proj, back, "round-trip must be identity");
assert!(
json.contains("title"),
"serialized JSON should contain field name"
);
}
#[test]
fn project_form_over_usaf_memo_fixture() {
let quill_path = quillmark_fixtures::resource_path("quills/usaf_memo/0.1.0");
let quill = Quillmark::new()
.quill_from_path(quill_path)
.expect("failed to load usaf_memo fixture");
let example_md = quill.source().example.as_deref().unwrap_or("");
let doc = match Document::from_markdown(example_md) {
Ok(d) => d,
Err(_) => return,
};
let proj = project_form(&quill, &doc);
assert!(
!proj.main.values.is_empty(),
"main card projection should have fields"
);
for (name, fv) in &proj.main.values {
match fv.source {
FormFieldSource::Document => {
assert!(
fv.value.is_some(),
"Document source must have value for {name}"
);
}
FormFieldSource::Default => {
assert!(
fv.value.is_none(),
"Default source must have no value for {name}"
);
assert!(
fv.default.is_some(),
"Default source must have default for {name}"
);
}
FormFieldSource::Missing => {
assert!(
fv.value.is_none(),
"Missing source must have no value for {name}"
);
assert!(
fv.default.is_none(),
"Missing source must have no default for {name}"
);
}
}
}
let json = serde_json::to_string(&proj).expect("projection must serialize");
assert!(!json.is_empty());
}