use std::path::{Path, PathBuf};
use scitadel_core::models::Paper;
use crate::sidecar::BibLockfile;
use crate::{export_bibtex_with_tags, export_csl_json_with_tags};
pub const SIDECAR_SUFFIX: &str = ".scitadel-bib.lock";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotFormat {
BibTeX,
CslJson,
}
impl SnapshotFormat {
#[must_use]
pub fn extension(self) -> &'static str {
match self {
Self::BibTeX => ".bib",
Self::CslJson => ".json",
}
}
}
#[must_use]
pub fn default_filename_for_format(format: SnapshotFormat) -> &'static Path {
match format {
SnapshotFormat::CslJson => Path::new("paper.json"),
SnapshotFormat::BibTeX => Path::new("paper.bib"),
}
}
#[must_use]
pub fn sidecar_path_for(bib: &Path) -> PathBuf {
let mut s = bib.as_os_str().to_owned();
s.push(SIDECAR_SUFFIX);
PathBuf::from(s)
}
#[derive(Debug, Clone)]
pub struct SnapshotOutcome {
pub output_path: PathBuf,
pub sidecar_path: Option<PathBuf>,
pub entry_count: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn write_snapshot<F>(
output_path: &Path,
question_id: &str,
reader: &str,
papers: &[Paper],
paper_ids: &[String],
tags_for: F,
format: SnapshotFormat,
write_lockfile: bool,
) -> std::io::Result<SnapshotOutcome>
where
F: Fn(&str) -> Vec<String>,
{
let (content, lock) = match format {
SnapshotFormat::BibTeX => {
let c = export_bibtex_with_tags(papers, &tags_for);
let l = BibLockfile::new_bibtex(question_id, reader, paper_ids, &c);
(c, l)
}
SnapshotFormat::CslJson => {
let c = export_csl_json_with_tags(papers, &tags_for);
let l = BibLockfile::new_csl_json(question_id, reader, paper_ids, &c);
(c, l)
}
};
std::fs::write(output_path, &content)?;
let sidecar_path = if write_lockfile {
let path = sidecar_path_for(output_path);
let json = lock.to_json().map_err(std::io::Error::other)?;
std::fs::write(&path, json)?;
Some(path)
} else {
None
};
Ok(SnapshotOutcome {
output_path: output_path.to_path_buf(),
sidecar_path,
entry_count: papers.len(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use scitadel_core::models::{Paper, PaperId};
fn paper(id: &str, title: &str, year: i32) -> Paper {
let mut p = Paper::new(title);
p.id = PaperId::from(id);
p.authors = vec!["Doe, J.".into()];
p.year = Some(year);
p
}
#[test]
fn sidecar_path_appends_suffix() {
let p = sidecar_path_for(Path::new("out/paper.bib"));
assert_eq!(p, PathBuf::from("out/paper.bib.scitadel-bib.lock"));
}
#[test]
fn default_filename_matches_format() {
assert_eq!(
default_filename_for_format(SnapshotFormat::BibTeX),
Path::new("paper.bib")
);
assert_eq!(
default_filename_for_format(SnapshotFormat::CslJson),
Path::new("paper.json")
);
}
#[test]
fn write_snapshot_bibtex_writes_both_files() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("p.bib");
let papers = vec![paper("p-1", "On Attention", 2017)];
let ids = vec!["p-1".to_string()];
let outcome = write_snapshot(
&out,
"q-x",
"lars",
&papers,
&ids,
|_| vec![],
SnapshotFormat::BibTeX,
true,
)
.unwrap();
assert_eq!(outcome.entry_count, 1);
assert_eq!(outcome.output_path, out);
let sidecar = outcome.sidecar_path.unwrap();
assert_eq!(sidecar, out.with_extension("bib.scitadel-bib.lock"));
let bib_bytes = std::fs::read_to_string(&out).unwrap();
assert!(bib_bytes.contains("On Attention"), "got: {bib_bytes}");
let lock_bytes = std::fs::read_to_string(&sidecar).unwrap();
let lock: BibLockfile = serde_json::from_str(&lock_bytes).unwrap();
assert_eq!(lock.question_id, "q-x");
assert_eq!(lock.format, "bibtex");
}
#[test]
fn write_snapshot_csl_json_writes_both_files() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("p.json");
let papers = vec![paper("p-1", "On Attention", 2017)];
let ids = vec!["p-1".to_string()];
let outcome = write_snapshot(
&out,
"q-x",
"lars",
&papers,
&ids,
|_| vec![],
SnapshotFormat::CslJson,
true,
)
.unwrap();
let lock: BibLockfile =
serde_json::from_str(&std::fs::read_to_string(outcome.sidecar_path.unwrap()).unwrap())
.unwrap();
assert_eq!(lock.format, "csl-json");
}
#[test]
fn write_snapshot_skips_sidecar_when_requested() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("p.bib");
let papers = vec![paper("p-1", "T", 2020)];
let ids = vec!["p-1".to_string()];
let outcome = write_snapshot(
&out,
"q",
"r",
&papers,
&ids,
|_| vec![],
SnapshotFormat::BibTeX,
false,
)
.unwrap();
assert!(outcome.sidecar_path.is_none());
assert!(out.exists());
}
}