#![allow(dead_code)]
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub enum BibtexEntryType {
Article,
Book,
InProceedings,
Misc,
TechReport,
PhDThesis,
}
impl BibtexEntryType {
pub fn keyword(&self) -> &'static str {
match self {
Self::Article => "article",
Self::Book => "book",
Self::InProceedings => "inproceedings",
Self::Misc => "misc",
Self::TechReport => "techreport",
Self::PhDThesis => "phdthesis",
}
}
}
#[derive(Debug, Clone)]
pub struct BibtexEntry {
pub entry_type: BibtexEntryType,
pub cite_key: String,
pub fields: BTreeMap<String, String>,
}
impl BibtexEntry {
pub fn new(entry_type: BibtexEntryType, cite_key: impl Into<String>) -> Self {
Self {
entry_type,
cite_key: cite_key.into(),
fields: BTreeMap::new(),
}
}
pub fn set_field(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.fields.insert(key.into(), value.into());
}
pub fn get_field(&self, key: &str) -> Option<&str> {
self.fields.get(key).map(String::as_str)
}
}
#[derive(Debug, Clone, Default)]
pub struct BibtexBibliography {
pub entries: Vec<BibtexEntry>,
}
impl BibtexBibliography {
pub fn add_entry(&mut self, entry: BibtexEntry) {
self.entries.push(entry);
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn find_by_key(&self, key: &str) -> Option<&BibtexEntry> {
self.entries.iter().find(|e| e.cite_key == key)
}
}
pub fn render_entry(entry: &BibtexEntry) -> String {
let mut out = format!("@{}{{{},\n", entry.entry_type.keyword(), entry.cite_key);
for (k, v) in &entry.fields {
out.push_str(&format!(" {k} = {{{v}}},\n"));
}
out.push_str("}\n");
out
}
pub fn render_bibtex(bib: &BibtexBibliography) -> String {
bib.entries
.iter()
.map(render_entry)
.collect::<Vec<_>>()
.join("\n")
}
pub fn validate_entry(entry: &BibtexEntry) -> bool {
!entry.cite_key.is_empty()
&& (entry.fields.contains_key("author") || entry.fields.contains_key("editor"))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_entry() -> BibtexEntry {
let mut e = BibtexEntry::new(BibtexEntryType::Article, "smith2026");
e.set_field("author", "Smith, John");
e.set_field("title", "A Survey");
e.set_field("year", "2026");
e
}
#[test]
fn entry_type_keyword() {
assert_eq!(BibtexEntryType::Article.keyword(), "article");
}
#[test]
fn set_and_get_field() {
let e = sample_entry();
assert_eq!(e.get_field("author"), Some("Smith, John"));
}
#[test]
fn entry_count() {
let mut bib = BibtexBibliography::default();
bib.add_entry(sample_entry());
assert_eq!(bib.entry_count(), 1);
}
#[test]
fn find_by_key() {
let mut bib = BibtexBibliography::default();
bib.add_entry(sample_entry());
assert!(bib.find_by_key("smith2026").is_some());
}
#[test]
fn find_missing_key() {
let bib = BibtexBibliography::default();
assert!(bib.find_by_key("nobody").is_none());
}
#[test]
fn render_entry_at_sign() {
let s = render_entry(&sample_entry());
assert!(s.starts_with('@'));
}
#[test]
fn render_entry_contains_key() {
assert!(render_entry(&sample_entry()).contains("smith2026"));
}
#[test]
fn validate_entry_ok() {
assert!(validate_entry(&sample_entry()));
}
#[test]
fn validate_no_author() {
let e = BibtexEntry::new(BibtexEntryType::Misc, "key");
assert!(!validate_entry(&e));
}
#[test]
fn render_bibtex_nonempty() {
let mut bib = BibtexBibliography::default();
bib.add_entry(sample_entry());
assert!(!render_bibtex(&bib).is_empty());
}
}