Files
brittle/brittle-core/src/bibtex/validation.rs
2026-03-25 09:32:02 +01:00

105 lines
3.7 KiB
Rust

use crate::error::BibtexError;
use crate::model::{EntryType, Reference};
/// Returns the required fields for a given BibTeX entry type.
fn required_fields(entry_type: &EntryType) -> &'static [&'static str] {
match entry_type {
EntryType::Article => &["author", "title", "journal", "year"],
EntryType::Book => &["title", "publisher", "year"],
EntryType::Booklet => &["title"],
EntryType::InBook => &["title", "publisher", "year", "chapter"],
EntryType::InCollection => &["author", "title", "booktitle", "publisher", "year"],
EntryType::InProceedings => &["author", "title", "booktitle", "year"],
EntryType::Manual => &["title"],
EntryType::MastersThesis => &["author", "title", "school", "year"],
EntryType::Misc => &[],
EntryType::PhdThesis => &["author", "title", "school", "year"],
EntryType::Proceedings => &["title", "year"],
EntryType::TechReport => &["author", "title", "institution", "year"],
EntryType::Unpublished => &["author", "title", "note"],
EntryType::Online => &["title", "url"],
}
}
/// Validate that a reference has all required fields for BibTeX export.
/// Returns an error describing the first missing required field found.
pub fn validate_for_export(reference: &Reference) -> Result<(), BibtexError> {
let required = required_fields(&reference.entry_type);
for &field in required {
let present = match field {
"author" => !reference.authors.is_empty(),
"editor" => !reference.editors.is_empty(),
_ => reference.fields.contains_key(field),
};
if !present {
return Err(BibtexError::MissingRequiredField {
cite_key: reference.cite_key.clone(),
entry_type: reference.entry_type.bibtex_name().to_owned(),
field: field.to_owned(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{EntryType, Person, Reference};
fn make_article() -> Reference {
let mut r = Reference::new("doe2024", EntryType::Article);
r.authors.push(Person::new("Doe"));
r.fields.insert("title".into(), "A Paper".into());
r.fields.insert("journal".into(), "Nature".into());
r.fields.insert("year".into(), "2024".into());
r
}
#[test]
fn valid_article_passes() {
let r = make_article();
assert!(validate_for_export(&r).is_ok());
}
#[test]
fn article_missing_author_fails() {
let mut r = make_article();
r.authors.clear();
let err = validate_for_export(&r).unwrap_err();
assert!(
matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "author")
);
}
#[test]
fn article_missing_journal_fails() {
let mut r = make_article();
r.fields.remove("journal");
let err = validate_for_export(&r).unwrap_err();
assert!(
matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "journal")
);
}
#[test]
fn misc_has_no_required_fields() {
let r = Reference::new("anon", EntryType::Misc);
assert!(validate_for_export(&r).is_ok());
}
#[test]
fn phd_thesis_requires_school() {
let mut r = Reference::new("smith2020", EntryType::PhdThesis);
r.authors.push(Person::new("Smith"));
r.fields.insert("title".into(), "A Thesis".into());
r.fields.insert("year".into(), "2020".into());
let err = validate_for_export(&r).unwrap_err();
assert!(
matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "school")
);
}
}