use std::path::Path;
use crate::diff::Entry;
use crate::import::parse::parse_bibtex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BibFormat {
Bibtex,
CslJson,
}
pub fn load_entries_from_path(path: &Path) -> Result<(Vec<Entry>, BibFormat), String> {
let bytes =
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
load_entries_from_str(&bytes).map_err(|e| format!("parse {}: {e}", path.display()))
}
pub fn load_entries_from_str(src: &str) -> Result<(Vec<Entry>, BibFormat), String> {
match detect_format_from_str(src) {
BibFormat::CslJson => {
let v: serde_json::Value =
serde_json::from_str(src).map_err(|e| format!("not valid JSON: {e}"))?;
let arr = v
.as_array()
.ok_or_else(|| "CSL-JSON top-level must be an array".to_string())?;
let entries: Vec<Entry> = arr.iter().filter_map(Entry::from_csl_value).collect();
Ok((entries, BibFormat::CslJson))
}
BibFormat::Bibtex => {
let parsed = parse_bibtex(src).map_err(|e| format!("BibTeX parse failed: {e}"))?;
let entries: Vec<Entry> = parsed.iter().map(Entry::from_bib_entry).collect();
Ok((entries, BibFormat::Bibtex))
}
}
}
#[must_use]
pub fn detect_format_from_str(src: &str) -> BibFormat {
if src.trim().is_empty() {
return BibFormat::Bibtex;
}
let first = src.trim_start().as_bytes().first().copied();
if matches!(first, Some(b'[' | b'{')) && serde_json::from_str::<serde_json::Value>(src).is_ok()
{
return BibFormat::CslJson;
}
BibFormat::Bibtex
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_csl_json_array() {
let s = r#"[{"id":"a","type":"article-journal","title":"T"}]"#;
assert_eq!(detect_format_from_str(s), BibFormat::CslJson);
}
#[test]
fn detects_bibtex_canonical() {
let s = r"@article{a, title = {T}, year = {2024}}";
assert_eq!(detect_format_from_str(s), BibFormat::Bibtex);
}
#[test]
fn empty_input_defaults_to_bibtex() {
assert_eq!(detect_format_from_str(""), BibFormat::Bibtex);
assert_eq!(detect_format_from_str(" \n "), BibFormat::Bibtex);
}
#[test]
fn malformed_json_falls_back_to_bibtex() {
let s = "[ this is not json {} ]";
assert_eq!(detect_format_from_str(s), BibFormat::Bibtex);
}
#[test]
fn content_sniff_overrides_extension_intent() {
let s = r#"[{"id":"x","type":"article-journal","title":"T"}]"#;
let (entries, fmt) = load_entries_from_str(s).unwrap();
assert_eq!(fmt, BibFormat::CslJson);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].citekey, "x");
}
#[test]
fn loads_bibtex_entries_into_neutral_shape() {
let s = r"
@article{smith2024,
title = {T},
author = {Smith, J.},
year = {2024},
doi = {10.1/ABC}
}";
let (entries, fmt) = load_entries_from_str(s).unwrap();
assert_eq!(fmt, BibFormat::Bibtex);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].citekey, "smith2024");
assert_eq!(entries[0].title.as_deref(), Some("T"));
assert_eq!(entries[0].doi.as_deref(), Some("10.1/abc"));
}
#[test]
fn loads_csl_json_entries_into_neutral_shape() {
let s = r#"[
{"id":"smith2024","type":"article-journal","title":"T",
"author":[{"family":"Smith","given":"J."}],
"issued":{"date-parts":[[2024]]},
"DOI":"10.1/ABC"}
]"#;
let (entries, fmt) = load_entries_from_str(s).unwrap();
assert_eq!(fmt, BibFormat::CslJson);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].citekey, "smith2024");
assert_eq!(entries[0].title.as_deref(), Some("T"));
assert_eq!(entries[0].doi.as_deref(), Some("10.1/abc"));
assert_eq!(entries[0].year, Some(2024));
}
#[test]
fn rejects_csl_top_level_object() {
let s = r#"{"id":"x","type":"article-journal","title":"T"}"#;
let err = load_entries_from_str(s).unwrap_err();
assert!(err.contains("must be an array"), "got: {err}");
}
}