use super::*;
use crate::Severity;
use std::collections::HashMap;
use std::error::Error as StdError;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn load_tree(path: &Path) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
let default_ignore = QuillIgnore::new(vec![
".git/".to_string(),
".gitignore".to_string(),
".quillignore".to_string(),
"target/".to_string(),
"node_modules/".to_string(),
]);
let quillignore_path = path.join(".quillignore");
let ignore = if quillignore_path.exists() {
let content = fs::read_to_string(&quillignore_path)?;
QuillIgnore::from_content(&content)
} else {
default_ignore
};
load_dir(path, path, &ignore)
}
fn load_dir(
current: &Path,
base: &Path,
ignore: &QuillIgnore,
) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
if !current.exists() {
return Ok(FileTreeNode::Directory {
files: HashMap::new(),
});
}
let mut files = HashMap::new();
for entry in fs::read_dir(current)? {
let entry = entry?;
let p = entry.path();
let rel = p.strip_prefix(base)?;
if ignore.is_ignored(rel) {
continue;
}
let name = p
.file_name()
.and_then(|n| n.to_str())
.ok_or("invalid filename")?
.to_string();
if p.is_file() {
files.insert(
name,
FileTreeNode::File {
contents: fs::read(&p)?,
},
);
} else if p.is_dir() {
files.insert(name, load_dir(&p, base, ignore)?);
}
}
Ok(FileTreeNode::Directory { files })
}
fn load_from_path<P: AsRef<Path>>(path: P) -> Result<QuillSource, Box<dyn StdError + Send + Sync>> {
let tree = load_tree(path.as_ref())?;
QuillSource::from_tree(tree)
}
#[test]
fn test_quillignore_parsing() {
let ignore_content = r#"
# This is a comment
*.tmp
target/
node_modules/
.git/
"#;
let ignore = QuillIgnore::from_content(ignore_content);
assert_eq!(ignore.patterns.len(), 4);
assert!(ignore.patterns.contains(&"*.tmp".to_string()));
assert!(ignore.patterns.contains(&"target/".to_string()));
}
#[test]
fn test_quillignore_matching() {
let ignore = QuillIgnore::new(vec![
"*.tmp".to_string(),
"target/".to_string(),
"node_modules/".to_string(),
".git/".to_string(),
]);
assert!(ignore.is_ignored("test.tmp"));
assert!(ignore.is_ignored("path/to/file.tmp"));
assert!(!ignore.is_ignored("test.txt"));
assert!(ignore.is_ignored("target"));
assert!(ignore.is_ignored("target/debug"));
assert!(ignore.is_ignored("target/debug/deps"));
assert!(!ignore.is_ignored("src/target.rs"));
assert!(ignore.is_ignored("node_modules"));
assert!(ignore.is_ignored("node_modules/package"));
assert!(!ignore.is_ignored("my_node_modules"));
}
#[test]
fn test_in_memory_file_system() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
fs::write(
quill_dir.join("Quill.yaml"),
"quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
)
.unwrap();
fs::write(quill_dir.join("plate.typ"), "test plate").unwrap();
let assets_dir = quill_dir.join("assets");
fs::create_dir_all(&assets_dir).unwrap();
fs::write(assets_dir.join("test.txt"), "asset content").unwrap();
let packages_dir = quill_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
fs::write(packages_dir.join("package.typ"), "package content").unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert!(quill.file_exists("plate.typ"));
assert!(quill.file_exists("assets/test.txt"));
assert!(quill.file_exists("packages/package.typ"));
assert!(!quill.file_exists("nonexistent.txt"));
let asset_content = quill.get_file("assets/test.txt").unwrap();
assert_eq!(asset_content, b"asset content");
let asset_files = quill.list_directory("assets");
assert_eq!(asset_files.len(), 1);
assert!(asset_files.contains(&PathBuf::from("assets/test.txt")));
}
#[test]
fn test_quillignore_integration() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
fs::write(quill_dir.join(".quillignore"), "*.tmp\ntarget/\n").unwrap();
fs::write(
quill_dir.join("Quill.yaml"),
"quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
)
.unwrap();
fs::write(quill_dir.join("plate.typ"), "test template").unwrap();
fs::write(quill_dir.join("should_ignore.tmp"), "ignored").unwrap();
let target_dir = quill_dir.join("target");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("debug.txt"), "also ignored").unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert!(quill.file_exists("plate.typ"));
assert!(!quill.file_exists("should_ignore.tmp"));
assert!(!quill.file_exists("target/debug.txt"));
}
#[test]
fn test_find_files_pattern() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
fs::write(
quill_dir.join("Quill.yaml"),
"quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
)
.unwrap();
fs::write(quill_dir.join("plate.typ"), "template").unwrap();
let assets_dir = quill_dir.join("assets");
fs::create_dir_all(&assets_dir).unwrap();
fs::write(assets_dir.join("image.png"), "png data").unwrap();
fs::write(assets_dir.join("data.json"), "json data").unwrap();
let fonts_dir = assets_dir.join("fonts");
fs::create_dir_all(&fonts_dir).unwrap();
fs::write(fonts_dir.join("font.ttf"), "font data").unwrap();
let quill = load_from_path(quill_dir).unwrap();
let all_assets = quill.find_files("assets/*");
assert!(all_assets.len() >= 3);
let typ_files = quill.find_files("*.typ");
assert_eq!(typ_files.len(), 1);
assert!(typ_files.contains(&PathBuf::from("plate.typ")));
}
#[test]
fn test_new_standardized_yaml_format() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"
quill:
name: my_custom_quill
version: "1.0"
backend: typst
plate_file: custom_plate.typ
description: Test quill with new format
author: Test Author
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
fs::write(
quill_dir.join("custom_plate.typ"),
"= Custom Template\n\nThis is a custom template.",
)
.unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert_eq!(quill.name, "my_custom_quill");
assert!(quill.metadata.contains_key("backend"));
if let Some(backend_val) = quill.metadata.get("backend") {
if let Some(backend_str) = backend_val.as_str() {
assert_eq!(backend_str, "typst");
} else {
panic!("Backend value is not a string");
}
}
assert!(quill.metadata.contains_key("description"));
assert!(quill.metadata.contains_key("author"));
assert!(quill.metadata.contains_key("version")); if let Some(version_val) = quill.metadata.get("version") {
if let Some(version_str) = version_val.as_str() {
assert_eq!(version_str, "1.0");
}
}
assert!(quill.plate.unwrap().contains("Custom Template"));
}
#[test]
fn test_template_loading() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"quill:
name: "test_with_template"
version: "1.0"
backend: "typst"
plate_file: "plate.typ"
example_file: "example.md"
description: "Test quill with template"
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
fs::write(
quill_dir.join("example.md"),
"---\ntitle: Test\n---\n\nThis is a test template.",
)
.unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert!(quill.example.is_some());
let example = quill.example.unwrap();
assert!(example.contains("title: Test"));
assert!(example.contains("This is a test template"));
assert!(quill
.config
.example_markdown
.as_ref()
.is_some_and(|value| value.contains("title: Test")));
assert_eq!(quill.plate.unwrap(), "plate content");
}
#[test]
fn test_template_smart_default() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"quill:
name: "test_smart_default"
version: "1.0"
backend: "typst"
plate_file: "plate.typ"
description: "Test quill with smart default"
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
fs::write(
quill_dir.join("example.md"),
"---\ntitle: Smart Default\n---\n\nPicked up automatically.",
)
.unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert!(quill.example.is_some());
let example = quill.example.unwrap();
assert!(example.contains("title: Smart Default"));
assert!(example.contains("Picked up automatically"));
}
#[test]
fn test_template_optional() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"quill:
name: "test_without_template"
version: "1.0"
backend: "typst"
plate_file: "plate.typ"
description: "Test quill without template"
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
let quill = load_from_path(quill_dir).unwrap();
assert_eq!(quill.example, None);
assert_eq!(quill.plate.unwrap(), "plate content");
}
#[test]
fn test_from_tree() {
let mut root_files = HashMap::new();
let quill_yaml = r#"quill:
name: "test_from_tree"
version: "1.0"
backend: "typst"
plate_file: "plate.typ"
description: "A test quill from tree"
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
let plate_content = "= Test Template\n\nThis is a test.";
root_files.insert(
"plate.typ".to_string(),
FileTreeNode::File {
contents: plate_content.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert_eq!(quill.name, "test_from_tree");
assert_eq!(quill.plate.unwrap(), plate_content);
assert!(quill.metadata.contains_key("backend"));
assert!(quill.metadata.contains_key("description"));
}
#[test]
fn test_from_tree_with_template() {
let mut root_files = HashMap::new();
let quill_yaml = r#"
quill:
name: test_tree_template
version: "1.0"
backend: typst
plate_file: plate.typ
example_file: template.md
description: Test tree with template
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
root_files.insert(
"plate.typ".to_string(),
FileTreeNode::File {
contents: b"plate content".to_vec(),
},
);
let template_content = "# {{ title }}\n\n{{ body }}";
root_files.insert(
"template.md".to_string(),
FileTreeNode::File {
contents: template_content.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert_eq!(quill.example, Some(template_content.to_string()));
}
#[test]
fn test_from_tree_structure_direct() {
let mut root_files = HashMap::new();
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents:
b"quill:\n name: direct_tree\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Direct tree test\n"
.to_vec(),
},
);
root_files.insert(
"plate.typ".to_string(),
FileTreeNode::File {
contents: b"plate content".to_vec(),
},
);
let mut src_files = HashMap::new();
src_files.insert(
"main.rs".to_string(),
FileTreeNode::File {
contents: b"fn main() {}".to_vec(),
},
);
root_files.insert(
"src".to_string(),
FileTreeNode::Directory { files: src_files },
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert_eq!(quill.name, "direct_tree");
assert!(quill.file_exists("src/main.rs"));
assert!(quill.file_exists("plate.typ"));
}
#[test]
fn test_dir_exists_and_list_apis() {
let mut root_files = HashMap::new();
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: b"quill:\n name: test\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Test quill\n"
.to_vec(),
},
);
root_files.insert(
"plate.typ".to_string(),
FileTreeNode::File {
contents: b"plate content".to_vec(),
},
);
let mut assets_files = HashMap::new();
assets_files.insert(
"logo.png".to_string(),
FileTreeNode::File {
contents: vec![137, 80, 78, 71],
},
);
assets_files.insert(
"icon.svg".to_string(),
FileTreeNode::File {
contents: b"<svg></svg>".to_vec(),
},
);
let mut fonts_files = HashMap::new();
fonts_files.insert(
"font.ttf".to_string(),
FileTreeNode::File {
contents: b"font data".to_vec(),
},
);
assets_files.insert(
"fonts".to_string(),
FileTreeNode::Directory { files: fonts_files },
);
root_files.insert(
"assets".to_string(),
FileTreeNode::Directory {
files: assets_files,
},
);
root_files.insert(
"empty".to_string(),
FileTreeNode::Directory {
files: HashMap::new(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert!(quill.dir_exists("assets"));
assert!(quill.dir_exists("assets/fonts"));
assert!(quill.dir_exists("empty"));
assert!(!quill.dir_exists("nonexistent"));
assert!(!quill.dir_exists("plate.typ"));
assert!(quill.file_exists("plate.typ"));
assert!(quill.file_exists("assets/logo.png"));
assert!(quill.file_exists("assets/fonts/font.ttf"));
assert!(!quill.file_exists("assets"));
let root_files_list = quill.list_files("");
assert_eq!(root_files_list.len(), 2); assert!(root_files_list.contains(&"Quill.yaml".to_string()));
assert!(root_files_list.contains(&"plate.typ".to_string()));
let assets_files_list = quill.list_files("assets");
assert_eq!(assets_files_list.len(), 2); assert!(assets_files_list.contains(&"logo.png".to_string()));
assert!(assets_files_list.contains(&"icon.svg".to_string()));
let root_subdirs = quill.list_subdirectories("");
assert_eq!(root_subdirs.len(), 2); assert!(root_subdirs.contains(&"assets".to_string()));
assert!(root_subdirs.contains(&"empty".to_string()));
let assets_subdirs = quill.list_subdirectories("assets");
assert_eq!(assets_subdirs.len(), 1); assert!(assets_subdirs.contains(&"fonts".to_string()));
let empty_subdirs = quill.list_subdirectories("empty");
assert_eq!(empty_subdirs.len(), 0);
}
#[test]
fn test_field_schemas_parsing() {
let mut root_files = HashMap::new();
let quill_yaml = r#"quill:
name: "taro"
version: "1.0"
backend: "typst"
plate_file: "plate.typ"
example_file: "taro.md"
description: "Test template for field schemas"
main:
fields:
author:
type: "string"
description: "Author of document"
ice_cream:
type: "string"
description: "favorite ice cream flavor"
title:
type: "string"
description: "title of document"
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
let plate_content = "= Test Template\n\nThis is a test.";
root_files.insert(
"plate.typ".to_string(),
FileTreeNode::File {
contents: plate_content.as_bytes().to_vec(),
},
);
root_files.insert(
"taro.md".to_string(),
FileTreeNode::File {
contents: b"# Template".to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert_eq!(quill.config.main.fields.len(), 3);
assert!(quill.config.main.fields.contains_key("author"));
assert!(quill.config.main.fields.contains_key("ice_cream"));
assert!(quill.config.main.fields.contains_key("title"));
let author_schema = quill.config.main.fields.get("author").unwrap();
assert_eq!(
author_schema.description.as_deref(),
Some("Author of document")
);
let ice_cream_schema = quill.config.main.fields.get("ice_cream").unwrap();
assert_eq!(
ice_cream_schema.description.as_deref(),
Some("favorite ice cream flavor")
);
let title_schema = quill.config.main.fields.get("title").unwrap();
assert_eq!(
title_schema.description.as_deref(),
Some("title of document")
);
}
#[test]
fn test_field_schema_struct() {
let schema1 = FieldSchema::new(
"test_name".to_string(),
FieldType::String,
Some("Test description".to_string()),
);
assert_eq!(schema1.description, Some("Test description".to_string()));
assert_eq!(schema1.r#type, FieldType::String);
assert_eq!(schema1.examples, None);
assert_eq!(schema1.default, None);
let yaml_str = r#"
description: "Full field schema"
type: "string"
examples:
- "Example value"
default: "Default value"
"#;
let quill_value = QuillValue::from_yaml_str(yaml_str).unwrap();
let schema2 = FieldSchema::from_quill_value("test_name".to_string(), &quill_value).unwrap();
assert_eq!(schema2.name, "test_name");
assert_eq!(schema2.description, Some("Full field schema".to_string()));
assert_eq!(schema2.r#type, FieldType::String);
assert_eq!(
schema2
.examples
.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|v| v.as_str()),
Some("Example value")
);
assert_eq!(
schema2.default.as_ref().and_then(|v| v.as_str()),
Some("Default value")
);
}
#[test]
fn test_field_schema_scalar_examples_coerced_to_array() {
let yaml_str = r#"
description: "Field schema with scalar example"
type: "date"
examples: "2024-01-15"
"#;
let quill_value = QuillValue::from_yaml_str(yaml_str).unwrap();
let schema = FieldSchema::from_quill_value("effective_date".to_string(), &quill_value).unwrap();
assert_eq!(
schema
.examples
.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|v| v.as_str()),
Some("2024-01-15")
);
}
#[test]
fn test_field_schema_ui_compact() {
let yaml_str = r#"
type: "string"
description: "A compact field"
ui:
compact: true
"#;
let quill_value = QuillValue::from_yaml_str(yaml_str).unwrap();
let schema = FieldSchema::from_quill_value("compact_field".to_string(), &quill_value).unwrap();
assert_eq!(schema.ui.as_ref().unwrap().compact, Some(true));
}
#[test]
fn test_quill_without_plate_file() {
let mut root_files = HashMap::new();
let quill_yaml = r#"quill:
name: "test_no_plate"
version: "1.0"
backend: "typst"
description: "Test quill without plate file"
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert!(quill.plate.clone().is_none());
assert_eq!(quill.name, "test_no_plate");
}
#[test]
fn test_quill_config_from_yaml() {
let yaml_content = r#"
quill:
name: test_config
version: "1.0"
backend: typst
description: Test configuration parsing
author: Test Author
plate_file: plate.typ
example_file: example.md
typst:
packages:
- "@preview/bubble:0.2.2"
main:
fields:
title:
description: Document title
type: string
author:
type: string
description: Document author
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
assert_eq!(config.name, "test_config");
assert_eq!(config.main.name, "main");
assert_eq!(config.backend, "typst");
assert_eq!(
config.main.description,
Some("Test configuration parsing".to_string())
);
assert_eq!(config.version, "1.0");
assert_eq!(config.author, "Test Author");
assert_eq!(config.plate_file, Some("plate.typ".to_string()));
assert_eq!(config.example_file, Some("example.md".to_string()));
assert!(config.typst_config.contains_key("packages"));
assert_eq!(config.main.fields.len(), 2);
assert!(config.main.fields.contains_key("title"));
assert!(config.main.fields.contains_key("author"));
let title_field = &config.main.fields["title"];
assert_eq!(title_field.description, Some("Document title".to_string()));
assert_eq!(title_field.r#type, FieldType::String);
}
#[test]
fn test_quill_config_parses_example_alias() {
let yaml_content = r#"
quill:
name: test_example_alias
version: "1.0"
backend: typst
description: Test example alias parsing
example: examples/basic.md
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
assert_eq!(config.example_file, Some("examples/basic.md".to_string()));
}
#[test]
fn test_quill_from_path_rejects_example_traversal() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"quill:
name: traversal_test
version: "1.0"
backend: typst
description: Traversal test
example: ../outside.md
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
let result = load_from_path(quill_dir);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside the quill directory"));
}
#[test]
fn test_quill_from_path_errors_when_explicit_example_missing() {
let temp_dir = TempDir::new().unwrap();
let quill_dir = temp_dir.path();
let yaml_content = r#"quill:
name: missing_example_test
version: "1.0"
backend: typst
description: Missing explicit example test
example: examples/missing.md
"#;
fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
let result = load_from_path(quill_dir);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("referenced in Quill.yaml not found"));
}
#[test]
fn test_quill_config_missing_required_fields() {
let yaml_missing_name = r#"
quill:
backend: typst
description: Missing name
"#;
let result = QuillConfig::from_yaml(yaml_missing_name);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required 'name'"));
let yaml_missing_backend = r#"
quill:
name: test
description: Missing backend
"#;
let result = QuillConfig::from_yaml(yaml_missing_backend);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required 'backend'"));
let yaml_missing_description = r#"
quill:
name: test
version: "1.0"
backend: typst
"#;
let result = QuillConfig::from_yaml(yaml_missing_description);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required 'description'"));
}
#[test]
fn test_quill_config_empty_description() {
let yaml_empty_description = r#"
quill:
name: test
version: "1.0"
backend: typst
description: " "
"#;
let result = QuillConfig::from_yaml(yaml_empty_description);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("description' field in 'quill' section cannot be empty"));
}
#[test]
fn test_quill_config_missing_quill_section() {
let yaml_no_section = r#"
fields:
title:
description: Title
"#;
let result = QuillConfig::from_yaml(yaml_no_section);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required 'quill' section"));
}
#[test]
fn test_quill_config_rejects_root_level_fields() {
let yaml = r#"
quill:
name: root_fields_test
version: "1.0"
backend: typst
description: Root fields must not be used
fields:
title:
type: string
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("main.fields"));
}
#[test]
fn test_quill_config_rejects_non_snake_case_quill_name() {
let yaml = r#"
quill:
name: BadQuill
version: "1.0"
backend: typst
description: Bad quill name
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("BadQuill"));
assert!(err.contains("snake_case"));
}
#[test]
fn test_quill_config_rejects_non_snake_case_card_name() {
let yaml = r#"
quill:
name: good_quill
version: "1.0"
backend: typst
description: Bad card name
card_types:
BadCard:
fields:
title:
type: string
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("BadCard"));
assert!(err.contains("[a-z_][a-z0-9_]*"));
}
#[test]
fn test_quill_config_accepts_leading_underscore_card_name() {
let yaml = r#"
quill:
name: good_quill
version: "1.0"
backend: typst
description: Leading underscore card name
card_types:
_private_card:
fields:
title:
type: string
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_ok());
}
#[test]
fn test_quill_config_rejects_non_snake_case_main_field_keys() {
let yaml = r#"
quill:
name: bad_field_key
version: "1.0"
backend: typst
description: Bad main field key
main:
fields:
BadField:
type: string
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("BadField"));
assert!(err.contains("snake_case"));
}
#[test]
fn test_quill_config_rejects_non_snake_case_card_field_keys() {
let yaml = r#"
quill:
name: bad_card_field_key
version: "1.0"
backend: typst
description: Bad card field key
card_types:
profile:
fields:
DisplayName:
type: string
"#;
let result = QuillConfig::from_yaml(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("DisplayName"));
assert!(err.contains("snake_case"));
}
#[test]
fn test_quill_from_config_metadata() {
let mut root_files = HashMap::new();
let quill_yaml = r#"
quill:
name: metadata_test
version: "1.0"
backend: typst
description: Test metadata flow
author: Test Author
custom_field: custom_value
typst:
packages:
- "@preview/bubble:0.2.2"
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
assert!(quill.metadata.contains_key("backend"));
assert!(quill.metadata.contains_key("description"));
assert!(quill.metadata.contains_key("author"));
assert!(quill.metadata.contains_key("custom_field"));
assert_eq!(
quill.metadata.get("custom_field").unwrap().as_str(),
Some("custom_value")
);
assert!(quill.metadata.contains_key("typst_packages"));
}
#[test]
fn test_config_defaults() {
let mut root_files = HashMap::new();
let quill_yaml = r#"
quill:
name: metadata_test_yaml
version: "1.0"
backend: typst
description: Test metadata flow
author: Test Author
custom_field: custom_value
typst:
packages:
- "@preview/bubble:0.2.2"
main:
fields:
author:
type: string
default: Anonymous
status:
type: string
default: draft
title:
type: string
"#;
root_files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: quill_yaml.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files: root_files };
let quill = QuillSource::from_tree(root).unwrap();
let defaults = quill.config.main.defaults();
assert_eq!(defaults.len(), 2);
assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
assert!(defaults.contains_key("status"));
assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
}
#[test]
fn test_config_defaults_and_examples_methods() {
let yaml_content = r#"
quill:
name: defaults_examples_test
version: "1.0"
backend: typst
description: Defaults and examples
main:
fields:
author:
type: string
default: Anonymous
examples:
- Alice
- Bob
status:
type: string
default: draft
title:
type: string
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let defaults = config.main.defaults();
let examples = config.main.examples();
assert_eq!(defaults.len(), 2);
assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
assert!(!defaults.contains_key("title"));
assert_eq!(examples.len(), 1);
let author_examples = examples.get("author").unwrap();
assert_eq!(author_examples.len(), 2);
assert_eq!(author_examples[0].as_str(), Some("Alice"));
assert_eq!(author_examples[1].as_str(), Some("Bob"));
}
#[test]
fn test_card_defaults_and_examples_methods() {
let yaml_content = r#"
quill:
name: card_defaults_examples_test
version: "1.0"
backend: typst
description: Card defaults and examples
card_types:
indorsement:
fields:
signature_block:
type: string
default: Commander
examples:
- Col Smith
office:
type: string
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let card = config.card_type("indorsement").unwrap();
let card_defaults = card.defaults();
assert_eq!(card_defaults.len(), 1);
assert_eq!(
card_defaults.get("signature_block").unwrap().as_str(),
Some("Commander")
);
let card_examples = card.examples();
assert_eq!(card_examples.len(), 1);
let signature_examples = card_examples.get("signature_block").unwrap();
assert_eq!(signature_examples.len(), 1);
assert_eq!(signature_examples[0].as_str(), Some("Col Smith"));
assert!(config.card_type("unknown").is_none());
}
#[test]
fn test_field_order_preservation() {
let yaml_content = r#"
quill:
name: order_test
version: "1.0"
backend: typst
description: Test field order
main:
fields:
first:
type: string
description: First field
second:
type: string
description: Second field
third:
type: string
description: Third field
ui:
group: Test Group
fourth:
type: string
description: Fourth field
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let first = config.main.fields.get("first").unwrap();
assert_eq!(first.ui.as_ref().unwrap().order, Some(0));
let second = config.main.fields.get("second").unwrap();
assert_eq!(second.ui.as_ref().unwrap().order, Some(1));
let third = config.main.fields.get("third").unwrap();
assert_eq!(third.ui.as_ref().unwrap().order, Some(2));
assert_eq!(
third.ui.as_ref().unwrap().group,
Some("Test Group".to_string())
);
let fourth = config.main.fields.get("fourth").unwrap();
assert_eq!(fourth.ui.as_ref().unwrap().order, Some(3));
}
#[test]
fn test_quill_with_all_ui_properties() {
let yaml_content = r#"
quill:
name: full_ui_test
version: "1.0"
backend: typst
description: Test all UI properties
main:
fields:
author:
description: The full name of the document author
type: string
ui:
group: Author Info
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let author_field = &config.main.fields["author"];
let ui = author_field.ui.as_ref().unwrap();
assert_eq!(ui.group, Some("Author Info".to_string()));
assert_eq!(ui.order, Some(0)); }
#[test]
fn test_field_schema_with_title_and_description() {
let yaml = r#"
title: "Field Title"
description: "Detailed field description"
type: "string"
examples:
- "Example value"
ui:
group: "Test Group"
"#;
let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
let schema = FieldSchema::from_quill_value("test_field".to_string(), &quill_value).unwrap();
assert_eq!(schema.title, Some("Field Title".to_string()));
assert_eq!(
schema.description,
Some("Detailed field description".to_string())
);
assert_eq!(
schema
.examples
.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|v| v.as_str()),
Some("Example value")
);
let ui = schema.ui.as_ref().unwrap();
assert_eq!(ui.group, Some("Test Group".to_string()));
}
#[test]
fn test_parse_card_field_type() {
let yaml = r#"
type: "string"
title: "Simple Field"
description: "A simple string field"
"#;
let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
let schema = FieldSchema::from_quill_value("simple_field".to_string(), &quill_value).unwrap();
assert_eq!(schema.name, "simple_field");
assert_eq!(schema.r#type, FieldType::String);
assert_eq!(schema.title, Some("Simple Field".to_string()));
assert_eq!(
schema.description,
Some("A simple string field".to_string())
);
}
#[test]
fn test_parse_card_with_fields_in_yaml() {
let yaml_content = r#"
quill:
name: cards_fields_test
version: "1.0"
backend: typst
description: Test [cards.X.fields.Y] syntax
card_types:
endorsements:
title: Endorsements
description: Chain of endorsements
fields:
name:
type: string
title: Endorser Name
description: Name of the endorsing official
required: true
org:
type: string
title: Organization
description: Endorser's organization
default: Unknown
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
assert!(config.card_type("endorsements").is_some());
let card = config.card_type("endorsements").unwrap();
assert_eq!(card.name, "endorsements");
assert_eq!(card.title, Some("Endorsements".to_string()));
assert_eq!(card.description, Some("Chain of endorsements".to_string()));
assert_eq!(card.fields.len(), 2);
let name_field = card.fields.get("name").unwrap();
assert_eq!(name_field.r#type, FieldType::String);
assert_eq!(name_field.title, Some("Endorser Name".to_string()));
assert!(name_field.required);
let org_field = card.fields.get("org").unwrap();
assert_eq!(org_field.r#type, FieldType::String);
assert!(org_field.default.is_some());
assert_eq!(
org_field.default.as_ref().unwrap().as_str(),
Some("Unknown")
);
}
#[test]
fn test_field_schema_rejects_unknown_keys() {
let yaml = r#"
type: "string"
description: "A string field"
invalid_key:
sub_field:
type: "string"
description: "Nested field"
"#;
let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
let result = FieldSchema::from_quill_value("author".to_string(), &quill_value);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("unknown field `invalid_key`"),
"Error was: {}",
err
);
}
#[test]
fn test_quill_config_with_cards_section() {
let yaml_content = r#"
quill:
name: cards_test
version: "1.0"
backend: typst
description: Test [cards] section
main:
fields:
regular:
description: Regular field
type: string
card_types:
indorsements:
title: Routing Indorsements
description: Chain of endorsements
fields:
name:
title: Name
type: string
description: Name field
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
assert!(config.main.fields.contains_key("regular"));
let regular = config.main.fields.get("regular").unwrap();
assert_eq!(regular.r#type, FieldType::String);
assert!(config.card_type("indorsements").is_some());
let card = config.card_type("indorsements").unwrap();
assert_eq!(card.title, Some("Routing Indorsements".to_string()));
assert_eq!(card.description, Some("Chain of endorsements".to_string()));
assert!(card.fields.contains_key("name"));
}
#[test]
fn test_quill_config_cards_empty_fields() {
let yaml_content = r#"
quill:
name: cards_empty_fields_test
version: "1.0"
backend: typst
description: Test cards without fields
card_types:
myscope:
description: My scope
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let card = config.card_type("myscope").unwrap();
assert_eq!(card.name, "myscope");
assert_eq!(card.description, Some("My scope".to_string()));
assert!(card.fields.is_empty());
}
#[test]
fn test_quill_config_allows_card_collision() {
let yaml_content = r#"
quill:
name: collision_test
version: "1.0"
backend: typst
description: Test collision
main:
fields:
conflict:
description: Field
type: string
card_types:
conflict:
description: Card
"#;
let result = QuillConfig::from_yaml(yaml_content);
if let Err(e) = &result {
panic!(
"Card name collision should be allowed, but got error: {}",
e
);
}
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.main.fields.contains_key("conflict"));
assert!(config.card_type("conflict").is_some());
}
#[test]
fn test_quill_config_ordering_with_cards() {
let yaml_content = r#"
quill:
name: ordering_test
version: "1.0"
backend: typst
description: Test ordering
main:
fields:
first:
type: string
description: First
zero:
type: string
description: Zero
card_types:
second:
description: Second
fields:
card_field:
type: string
description: A card field
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let first = config.main.fields.get("first").unwrap();
let zero = config.main.fields.get("zero").unwrap();
let second = config.card_type("second").unwrap();
let ord_first = first.ui.as_ref().unwrap().order.unwrap();
let ord_zero = zero.ui.as_ref().unwrap().order.unwrap();
assert!(ord_first < ord_zero);
assert_eq!(ord_first, 0);
assert_eq!(ord_zero, 1);
let card_field = second.fields.get("card_field").unwrap();
let ord_card_field = card_field.ui.as_ref().unwrap().order.unwrap();
assert_eq!(ord_card_field, 0); }
#[test]
fn test_card_field_order_preservation() {
let yaml_content = r#"
quill:
name: card_order_test
version: "1.0"
backend: typst
description: Test card field order
card_types:
mycard:
description: Test card
fields:
z_first:
type: string
description: Defined first
a_second:
type: string
description: Defined second
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let card = config.card_type("mycard").unwrap();
let z_first = card.fields.get("z_first").unwrap();
let a_second = card.fields.get("a_second").unwrap();
let z_order = z_first.ui.as_ref().unwrap().order.unwrap();
let a_order = a_second.ui.as_ref().unwrap().order.unwrap();
assert_eq!(z_order, 0, "z_first should be 0 (defined first)");
assert_eq!(a_order, 1, "a_second should be 1 (defined second)");
}
#[test]
fn test_nested_schema_parsing() {
let yaml_content = r#"
quill:
name: nested_test
version: "1.0"
backend: typst
description: Test nested elements
main:
fields:
my_list:
type: array
description: List of objects
items:
type: object
properties:
sub_a:
type: string
description: Subfield A
sub_b:
type: number
description: Subfield B
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let list_field = config.main.fields.get("my_list").unwrap();
assert_eq!(list_field.r#type, FieldType::Array);
assert!(list_field.items.is_some());
let items_schema = list_field.items.as_ref().unwrap();
assert_eq!(items_schema.r#type, FieldType::Object);
assert!(items_schema.properties.is_some());
let props = items_schema.properties.as_ref().unwrap();
assert!(props.contains_key("sub_a"));
assert!(props.contains_key("sub_b"));
assert_eq!(props["sub_a"].r#type, FieldType::String);
assert_eq!(props["sub_b"].r#type, FieldType::Number);
}
#[test]
fn test_standalone_object_field_rejected_with_warning() {
let yaml_content = r#"
quill:
name: obj_test
version: "1.0"
backend: typst
description: Test standalone object rejection
main:
fields:
valid_field:
type: string
description: A normal field
address:
type: object
description: Standalone object — should be rejected
properties:
street:
type: string
"#;
let (config, warnings) = QuillConfig::from_yaml_with_warnings(yaml_content).unwrap();
assert!(config.main.fields.contains_key("valid_field"));
assert!(!config.main.fields.contains_key("address"));
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].severity, Severity::Warning);
assert_eq!(
warnings[0].code.as_deref(),
Some("quill::standalone_object_not_supported")
);
assert!(warnings[0].message.contains("address"));
}
#[test]
fn test_nested_object_in_typed_table_rejected_with_warning() {
let yaml_content = r#"
quill:
name: nested_obj_test
version: "1.0"
backend: typst
description: Test nested object in typed table rejection
main:
fields:
rows:
type: array
items:
type: object
properties:
score:
type: number
nested:
type: object
properties:
inner:
type: string
"#;
let (config, warnings) = QuillConfig::from_yaml_with_warnings(yaml_content).unwrap();
assert!(!config.main.fields.contains_key("rows"));
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].severity, Severity::Warning);
assert_eq!(
warnings[0].code.as_deref(),
Some("quill::nested_object_not_supported")
);
assert!(warnings[0].message.contains("rows"));
}
#[test]
fn test_array_items_recursive_coercion() {
let yaml_content = r#"
quill:
name: coerce_test
version: "1.0"
backend: typst
description: Test recursive coercion for array items
main:
fields:
scores:
type: array
items:
type: object
properties:
name:
type: string
value:
type: number
active:
type: boolean
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"scores".to_string(),
crate::value::QuillValue::from_json(serde_json::json!([
{"name": "Math", "value": "95", "active": "true"},
{"name": "Science", "value": "88.5", "active": "false"}
])),
);
let coerced = config.coerce_frontmatter(&frontmatter).unwrap();
let scores = coerced.get("scores").unwrap();
let arr = scores.as_array().unwrap();
let first = arr[0].as_object().unwrap();
assert_eq!(first["name"], serde_json::json!("Math"));
assert_eq!(first["value"], serde_json::json!(95)); assert_eq!(first["active"], serde_json::json!(true));
let second = arr[1].as_object().unwrap();
assert_eq!(second["value"], serde_json::json!(88.5)); assert_eq!(second["active"], serde_json::json!(false)); }
#[test]
fn test_config_coerce_number_boolean_date_datetime_success() {
let yaml_content = r#"
quill:
name: coerce_success_test
version: "1.0"
backend: typst
description: Coerce success
main:
fields:
count:
type: number
active:
type: boolean
signed_on:
type: date
created_at:
type: datetime
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"count".to_string(),
QuillValue::from_json(serde_json::json!("42")),
);
frontmatter.insert(
"active".to_string(),
QuillValue::from_json(serde_json::json!("true")),
);
frontmatter.insert(
"signed_on".to_string(),
QuillValue::from_json(serde_json::json!("2026-04-13")),
);
frontmatter.insert(
"created_at".to_string(),
QuillValue::from_json(serde_json::json!("2026-04-13T20:00:00Z")),
);
let coerced = config.coerce_frontmatter(&frontmatter).unwrap();
assert_eq!(coerced.get("count").unwrap().as_i64(), Some(42));
assert_eq!(coerced.get("active").unwrap().as_bool(), Some(true));
assert_eq!(
coerced.get("signed_on").unwrap().as_str(),
Some("2026-04-13")
);
assert_eq!(
coerced.get("created_at").unwrap().as_str(),
Some("2026-04-13T20:00:00Z")
);
}
#[test]
fn test_config_coerce_integer_success() {
let yaml_content = r#"
quill:
name: coerce_integer_success_test
version: "1.0"
backend: typst
description: Coerce integer success
main:
fields:
count:
type: integer
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"count".to_string(),
QuillValue::from_json(serde_json::json!("42")),
);
let coerced = config.coerce_frontmatter(&frontmatter).unwrap();
assert_eq!(coerced.get("count").unwrap().as_i64(), Some(42));
}
#[test]
fn test_config_coerce_integer_rejects_decimal() {
let yaml_content = r#"
quill:
name: coerce_integer_error_test
version: "1.0"
backend: typst
description: Coerce integer errors
main:
fields:
count:
type: integer
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"count".to_string(),
QuillValue::from_json(serde_json::json!("42.5")),
);
let error = config.coerce_frontmatter(&frontmatter).unwrap_err();
assert!(matches!(
error,
super::CoercionError::Uncoercible { ref path, ref target, .. }
if path == "count" && target == "integer"
));
}
#[test]
fn test_config_coerce_array_item_wise() {
let yaml_content = r#"
quill:
name: coerce_array_items_test
version: "1.0"
backend: typst
description: Coerce arrays
main:
fields:
items:
type: array
items:
type: object
properties:
score:
type: number
active:
type: boolean
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"items".to_string(),
QuillValue::from_json(serde_json::json!([
{"score": "90", "active": "true"},
{"score": "87.5", "active": "false"}
])),
);
let coerced = config.coerce_frontmatter(&frontmatter).unwrap();
let items = coerced.get("items").unwrap().as_array().unwrap();
let first = items[0].as_object().unwrap();
let second = items[1].as_object().unwrap();
assert_eq!(first["score"], serde_json::json!(90));
assert_eq!(first["active"], serde_json::json!(true));
assert_eq!(second["score"], serde_json::json!(87.5));
assert_eq!(second["active"], serde_json::json!(false));
}
#[test]
fn test_config_coerce_cards_item_wise() {
let yaml_content = r#"
quill:
name: coerce_cards_items_test
version: "1.0"
backend: typst
description: Coerce cards
card_types:
indorsement:
fields:
score:
type: number
active:
type: boolean
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut card_fields = indexmap::IndexMap::new();
card_fields.insert(
"score".to_string(),
QuillValue::from_json(serde_json::json!("100")),
);
card_fields.insert(
"active".to_string(),
QuillValue::from_json(serde_json::json!("false")),
);
let coerced = config.coerce_card("indorsement", &card_fields).unwrap();
assert_eq!(coerced.get("score").unwrap().as_i64(), Some(100));
assert_eq!(coerced.get("active").unwrap().as_bool(), Some(false));
}
#[test]
fn test_config_coerce_error_unparseable_date() {
let yaml_content = r#"
quill:
name: coerce_date_error_test
version: "1.0"
backend: typst
description: Coerce date errors
main:
fields:
signed_on:
type: date
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"signed_on".to_string(),
QuillValue::from_json(serde_json::json!("13-04-2026")),
);
let error = config.coerce_frontmatter(&frontmatter).unwrap_err();
assert!(matches!(
error,
super::CoercionError::Uncoercible { ref path, ref target, .. }
if path == "signed_on" && target == "date"
));
}
#[test]
fn test_config_coerce_error_unparseable_number() {
let yaml_content = r#"
quill:
name: coerce_number_error_test
version: "1.0"
backend: typst
description: Coerce number errors
main:
fields:
count:
type: number
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let mut frontmatter = indexmap::IndexMap::new();
frontmatter.insert(
"count".to_string(),
QuillValue::from_json(serde_json::json!("forty-two")),
);
let error = config.coerce_frontmatter(&frontmatter).unwrap_err();
assert!(matches!(
error,
super::CoercionError::Uncoercible { ref path, ref target, .. }
if path == "count" && target == "number"
));
}
#[test]
fn test_multiline_ui_field_parses() {
let yaml_content = r#"
quill:
name: multiline_test
version: "1.0"
backend: typst
description: Test multiline ui hint
main:
fields:
summary:
type: markdown
description: Document summary
ui:
multiline: true
notes:
type: markdown
description: Short notes
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let summary = config.main.fields.get("summary").unwrap();
assert_eq!(summary.r#type, FieldType::Markdown);
assert_eq!(summary.ui.as_ref().unwrap().multiline, Some(true));
let notes = config.main.fields.get("notes").unwrap();
assert_eq!(notes.r#type, FieldType::Markdown);
assert_eq!(notes.ui.as_ref().unwrap().multiline, None);
}
#[test]
fn test_multiline_ui_field_on_string_type() {
let yaml_content = r#"
quill:
name: multiline_string_test
version: "1.0"
backend: typst
description: Test multiline ui hint on string field
main:
fields:
address:
type: string
description: Mailing address
ui:
multiline: true
name:
type: string
description: Full name
"#;
let config = QuillConfig::from_yaml(yaml_content).unwrap();
let address = config.main.fields.get("address").unwrap();
assert_eq!(address.r#type, FieldType::String);
assert_eq!(address.ui.as_ref().unwrap().multiline, Some(true));
let name = config.main.fields.get("name").unwrap();
assert_eq!(name.r#type, FieldType::String);
assert!(name.ui.as_ref().map_or(true, |ui| ui.multiline.is_none()));
}
#[test]
fn test_quill_config_from_yaml_collects_non_fatal_field_warnings() {
let yaml_content = r#"
quill:
name: warning_config
version: "1.0"
backend: typst
description: Warning collection test
main:
fields:
valid_field:
type: string
description: Valid
broken_field:
description: Missing required type
"#;
let (config, warnings) = QuillConfig::from_yaml_with_warnings(yaml_content).unwrap();
assert!(config.main.fields.contains_key("valid_field"));
assert!(!config.main.fields.contains_key("broken_field"));
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].severity, Severity::Warning);
assert_eq!(
warnings[0].code.as_deref(),
Some("quill::field_parse_warning")
);
assert!(warnings[0]
.message
.contains("Failed to parse field schema 'broken_field'"));
}
#[test]
fn public_schema_snapshot_usaf_memo_0_1_0() {
let quill_path = quillmark_fixtures::resource_path("quills/usaf_memo/0.1.0");
let quill = load_from_path(quill_path).expect("failed to load usaf_memo fixture");
let yaml = quill
.config
.public_schema_yaml()
.expect("failed to emit public schema yaml");
let expected_path =
quillmark_fixtures::resource_path("quills/usaf_memo/0.1.0/__golden__/public_schema.yaml");
if std::env::var("UPDATE_GOLDEN").is_ok() {
fs::write(&expected_path, &yaml).expect("failed to update golden public schema");
}
let expected = fs::read_to_string(&expected_path).expect("failed to read golden public schema");
assert_eq!(yaml, expected, "public schema snapshot changed");
let parsed: serde_json::Value =
serde_saphyr::from_str(&yaml).expect("schema yaml should parse");
assert!(parsed.get("name").is_some());
assert!(parsed.get("main").is_some());
assert!(
parsed.get("main").and_then(|v| v.get("fields")).is_some(),
"main.fields should be present"
);
assert!(parsed.get("card_types").is_some());
assert!(parsed.get("CARDS").is_none());
let value = quill.config.public_schema();
assert_eq!(value, parsed);
}