scitadel_export/
snapshot.rs1use std::path::{Path, PathBuf};
15
16use scitadel_core::models::Paper;
17
18use crate::sidecar::BibLockfile;
19use crate::{export_bibtex_with_tags, export_csl_json_with_tags};
20
21pub const SIDECAR_SUFFIX: &str = ".scitadel-bib.lock";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SnapshotFormat {
31 BibTeX,
32 CslJson,
33}
34
35impl SnapshotFormat {
36 #[must_use]
39 pub fn extension(self) -> &'static str {
40 match self {
41 Self::BibTeX => ".bib",
42 Self::CslJson => ".json",
43 }
44 }
45}
46
47#[must_use]
52pub fn default_filename_for_format(format: SnapshotFormat) -> &'static Path {
53 match format {
54 SnapshotFormat::CslJson => Path::new("paper.json"),
55 SnapshotFormat::BibTeX => Path::new("paper.bib"),
56 }
57}
58
59#[must_use]
63pub fn sidecar_path_for(bib: &Path) -> PathBuf {
64 let mut s = bib.as_os_str().to_owned();
65 s.push(SIDECAR_SUFFIX);
66 PathBuf::from(s)
67}
68
69#[derive(Debug, Clone)]
73pub struct SnapshotOutcome {
74 pub output_path: PathBuf,
77 pub sidecar_path: Option<PathBuf>,
80 pub entry_count: usize,
83}
84
85#[allow(clippy::too_many_arguments)]
97pub fn write_snapshot<F>(
98 output_path: &Path,
99 question_id: &str,
100 reader: &str,
101 papers: &[Paper],
102 paper_ids: &[String],
103 tags_for: F,
104 format: SnapshotFormat,
105 write_lockfile: bool,
106) -> std::io::Result<SnapshotOutcome>
107where
108 F: Fn(&str) -> Vec<String>,
109{
110 let (content, lock) = match format {
111 SnapshotFormat::BibTeX => {
112 let c = export_bibtex_with_tags(papers, &tags_for);
113 let l = BibLockfile::new_bibtex(question_id, reader, paper_ids, &c);
114 (c, l)
115 }
116 SnapshotFormat::CslJson => {
117 let c = export_csl_json_with_tags(papers, &tags_for);
118 let l = BibLockfile::new_csl_json(question_id, reader, paper_ids, &c);
119 (c, l)
120 }
121 };
122
123 std::fs::write(output_path, &content)?;
124
125 let sidecar_path = if write_lockfile {
126 let path = sidecar_path_for(output_path);
127 let json = lock.to_json().map_err(std::io::Error::other)?;
128 std::fs::write(&path, json)?;
129 Some(path)
130 } else {
131 None
132 };
133
134 Ok(SnapshotOutcome {
135 output_path: output_path.to_path_buf(),
136 sidecar_path,
137 entry_count: papers.len(),
138 })
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use scitadel_core::models::{Paper, PaperId};
145
146 fn paper(id: &str, title: &str, year: i32) -> Paper {
147 let mut p = Paper::new(title);
148 p.id = PaperId::from(id);
149 p.authors = vec!["Doe, J.".into()];
150 p.year = Some(year);
151 p
152 }
153
154 #[test]
155 fn sidecar_path_appends_suffix() {
156 let p = sidecar_path_for(Path::new("out/paper.bib"));
157 assert_eq!(p, PathBuf::from("out/paper.bib.scitadel-bib.lock"));
158 }
159
160 #[test]
161 fn default_filename_matches_format() {
162 assert_eq!(
163 default_filename_for_format(SnapshotFormat::BibTeX),
164 Path::new("paper.bib")
165 );
166 assert_eq!(
167 default_filename_for_format(SnapshotFormat::CslJson),
168 Path::new("paper.json")
169 );
170 }
171
172 #[test]
173 fn write_snapshot_bibtex_writes_both_files() {
174 let dir = tempfile::tempdir().unwrap();
175 let out = dir.path().join("p.bib");
176 let papers = vec![paper("p-1", "On Attention", 2017)];
177 let ids = vec!["p-1".to_string()];
178
179 let outcome = write_snapshot(
180 &out,
181 "q-x",
182 "lars",
183 &papers,
184 &ids,
185 |_| vec![],
186 SnapshotFormat::BibTeX,
187 true,
188 )
189 .unwrap();
190
191 assert_eq!(outcome.entry_count, 1);
192 assert_eq!(outcome.output_path, out);
193 let sidecar = outcome.sidecar_path.unwrap();
194 assert_eq!(sidecar, out.with_extension("bib.scitadel-bib.lock"));
195 let bib_bytes = std::fs::read_to_string(&out).unwrap();
196 assert!(bib_bytes.contains("On Attention"), "got: {bib_bytes}");
197 let lock_bytes = std::fs::read_to_string(&sidecar).unwrap();
198 let lock: BibLockfile = serde_json::from_str(&lock_bytes).unwrap();
199 assert_eq!(lock.question_id, "q-x");
200 assert_eq!(lock.format, "bibtex");
201 }
202
203 #[test]
204 fn write_snapshot_csl_json_writes_both_files() {
205 let dir = tempfile::tempdir().unwrap();
206 let out = dir.path().join("p.json");
207 let papers = vec![paper("p-1", "On Attention", 2017)];
208 let ids = vec!["p-1".to_string()];
209
210 let outcome = write_snapshot(
211 &out,
212 "q-x",
213 "lars",
214 &papers,
215 &ids,
216 |_| vec![],
217 SnapshotFormat::CslJson,
218 true,
219 )
220 .unwrap();
221
222 let lock: BibLockfile =
223 serde_json::from_str(&std::fs::read_to_string(outcome.sidecar_path.unwrap()).unwrap())
224 .unwrap();
225 assert_eq!(lock.format, "csl-json");
226 }
227
228 #[test]
229 fn write_snapshot_skips_sidecar_when_requested() {
230 let dir = tempfile::tempdir().unwrap();
231 let out = dir.path().join("p.bib");
232 let papers = vec![paper("p-1", "T", 2020)];
233 let ids = vec!["p-1".to_string()];
234
235 let outcome = write_snapshot(
236 &out,
237 "q",
238 "r",
239 &papers,
240 &ids,
241 |_| vec![],
242 SnapshotFormat::BibTeX,
243 false,
244 )
245 .unwrap();
246
247 assert!(outcome.sidecar_path.is_none());
248 assert!(out.exists());
249 }
250}