1use crate::sources::Paper;
2
3pub fn to_csv(papers: &[Paper]) -> String {
5 let mut out = String::from("id,title,authors,year,doi,url\n");
6 for p in papers {
7 out.push_str(&format!(
8 "{},{},{},{},{},{}\n",
9 csv_escape(&p.id),
10 csv_escape(&p.title),
11 csv_escape(&p.authors.join(";")),
12 p.year.map(|y| y.to_string()).unwrap_or_default(),
13 csv_escape(p.doi.as_deref().unwrap_or("")),
14 csv_escape(p.url.as_deref().unwrap_or("")),
15 ));
16 }
17 out
18}
19
20fn csv_escape(field: &str) -> String {
21 if field.contains(',') || field.contains('"') || field.contains('\n') {
22 format!("\"{}\"", field.replace('"', "\"\""))
23 } else {
24 field.to_string()
25 }
26}
27
28pub fn to_bibtex(papers: &[Paper]) -> String {
30 let mut out = String::new();
31 for p in papers {
32 out.push_str(&format!("@article{{{},\n", p.id));
33 out.push_str(&format!(" title = {{{}}},\n", p.title));
34 out.push_str(&format!(" author = {{{}}},\n", p.authors.join(" and ")));
35 if let Some(year) = p.year {
36 out.push_str(&format!(" year = {{{}}},\n", year));
37 }
38 if let Some(ref doi) = p.doi {
39 out.push_str(&format!(" doi = {{{}}},\n", doi));
40 }
41 out.push_str("}\n");
42 }
43 out
44}
45
46pub fn to_table(papers: &[Paper]) -> String {
48 if papers.is_empty() {
49 return "No results found".to_string();
50 }
51 let mut out = String::new();
52 for (i, p) in papers.iter().enumerate() {
53 if i > 0 {
54 out.push('\n');
55 }
56 out.push_str(&format!("[{}] {}", p.id, p.title));
57 let mut meta = Vec::new();
58 if let Some(year) = p.year {
59 meta.push(year.to_string());
60 }
61 if !p.authors.is_empty() {
62 meta.push(p.authors.join(", "));
63 }
64 if !meta.is_empty() {
65 out.push_str(&format!("\n {}", meta.join(" | ")));
66 }
67 }
68 out.push('\n');
69 out
70}
71
72pub fn to_json(papers: &[Paper]) -> String {
74 let result = serde_json::json!({
75 "source": papers.first().map(|p| p.source.as_str()).unwrap_or(""),
76 "results": papers,
77 });
78 serde_json::to_string_pretty(&result).unwrap()
79}
80
81pub fn to_jsonl(papers: &[Paper]) -> String {
86 let mut out = String::new();
87 for paper in papers {
88 out.push_str(&serde_json::to_string(paper).unwrap());
89 out.push('\n');
90 }
91 out
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn sample_paper() -> Paper {
99 Paper {
100 id: "2301.08745".to_string(),
101 title: "Attention Is All You Need".to_string(),
102 authors: vec!["Alice Smith".to_string(), "Bob Jones".to_string()],
103 abstract_text: Some("We propose a new architecture.".to_string()),
104 year: Some(2023),
105 doi: Some("10.48550/arXiv.2301.08745".to_string()),
106 url: Some("https://arxiv.org/abs/2301.08745".to_string()),
107 pdf_url: Some("https://arxiv.org/pdf/2301.08745".to_string()),
108 venue: Some("arXiv preprint".to_string()),
109 citations: None,
110 fields: vec!["cs.CL".to_string()],
111 open_access: Some(true),
112 source: "arxiv".to_string(),
113 }
114 }
115
116 #[test]
117 fn to_json_returns_valid_json() {
118 let json = to_json(&[sample_paper()]);
119 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should be valid JSON");
120 assert!(parsed.is_object());
121 }
122
123 #[test]
124 fn to_json_has_source_field() {
125 let json = to_json(&[sample_paper()]);
126 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
127 assert_eq!(v["source"], "arxiv");
128 }
129
130 #[test]
131 fn to_json_has_results_array() {
132 let json = to_json(&[sample_paper()]);
133 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
134 let results = v["results"].as_array().expect("results should be array");
135 assert_eq!(results.len(), 1);
136 }
137
138 #[test]
139 fn to_json_paper_has_expected_fields() {
140 let json = to_json(&[sample_paper()]);
141 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
142 let paper = &v["results"][0];
143 assert_eq!(paper["id"], "2301.08745");
144 assert_eq!(paper["title"], "Attention Is All You Need");
145 assert_eq!(paper["authors"][0], "Alice Smith");
146 assert_eq!(paper["year"], 2023);
147 assert_eq!(paper["doi"], "10.48550/arXiv.2301.08745");
148 assert_eq!(paper["url"], "https://arxiv.org/abs/2301.08745");
149 }
150
151 #[test]
152 fn to_json_empty_papers() {
153 let json = to_json(&[]);
154 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
155 let results = v["results"].as_array().expect("results should be array");
156 assert!(results.is_empty());
157 }
158
159 #[test]
160 fn to_csv_first_line_is_header() {
161 let csv = to_csv(&[sample_paper()]);
162 let first_line = csv.lines().next().unwrap();
163 assert_eq!(first_line, "id,title,authors,year,doi,url");
164 }
165
166 #[test]
167 fn to_csv_second_line_contains_id() {
168 let csv = to_csv(&[sample_paper()]);
169 let second_line = csv.lines().nth(1).unwrap();
170 assert!(second_line.starts_with("2301.08745,"));
171 }
172
173 #[test]
174 fn to_csv_authors_joined_by_semicolon() {
175 let csv = to_csv(&[sample_paper()]);
176 let second_line = csv.lines().nth(1).unwrap();
177 assert!(second_line.contains("Alice Smith;Bob Jones"));
178 }
179
180 #[test]
181 fn to_csv_quotes_field_with_comma() {
182 let mut paper = sample_paper();
183 paper.title = "Attention, Transformers, and You".to_string();
184 let csv = to_csv(&[paper]);
185 let second_line = csv.lines().nth(1).unwrap();
186 assert!(second_line.contains("\"Attention, Transformers, and You\""));
187 }
188
189 #[test]
190 fn to_csv_empty_returns_header_only() {
191 let csv = to_csv(&[]);
192 let lines: Vec<&str> = csv.lines().collect();
193 assert_eq!(lines.len(), 1);
194 assert_eq!(lines[0], "id,title,authors,year,doi,url");
195 }
196
197 #[test]
198 fn to_bibtex_contains_article_tag() {
199 let bib = to_bibtex(&[sample_paper()]);
200 assert!(bib.contains("@article{"));
201 }
202
203 #[test]
204 fn to_bibtex_contains_title() {
205 let bib = to_bibtex(&[sample_paper()]);
206 assert!(bib.contains("title = {Attention Is All You Need}"));
207 }
208
209 #[test]
210 fn to_bibtex_contains_author() {
211 let bib = to_bibtex(&[sample_paper()]);
212 assert!(bib.contains("author = {Alice Smith and Bob Jones}"));
213 }
214
215 #[test]
216 fn to_bibtex_contains_year() {
217 let bib = to_bibtex(&[sample_paper()]);
218 assert!(bib.contains("year = {2023}"));
219 }
220
221 #[test]
222 fn to_bibtex_contains_doi() {
223 let bib = to_bibtex(&[sample_paper()]);
224 assert!(bib.contains("doi = {10.48550/arXiv.2301.08745}"));
225 }
226
227 #[test]
228 fn to_bibtex_no_doi_when_missing() {
229 let mut paper = sample_paper();
230 paper.doi = None;
231 let bib = to_bibtex(&[paper]);
232 assert!(!bib.contains("doi ="));
233 }
234
235 #[test]
236 fn to_bibtex_empty_returns_empty_string() {
237 let bib = to_bibtex(&[]);
238 assert!(bib.is_empty());
239 }
240
241 #[test]
242 fn to_table_contains_title() {
243 let table = to_table(&[sample_paper()]);
244 assert!(table.contains("Attention Is All You Need"));
245 }
246
247 #[test]
248 fn to_table_contains_id() {
249 let table = to_table(&[sample_paper()]);
250 assert!(table.contains("2301.08745"));
251 }
252
253 #[test]
254 fn to_table_contains_year() {
255 let table = to_table(&[sample_paper()]);
256 assert!(table.contains("2023"));
257 }
258
259 #[test]
260 fn to_table_multiple_papers_on_separate_lines() {
261 let mut p2 = sample_paper();
262 p2.id = "2312.00001".to_string();
263 p2.title = "Second Paper".to_string();
264 let table = to_table(&[sample_paper(), p2]);
265 assert!(table.contains("Attention Is All You Need"));
266 assert!(table.contains("Second Paper"));
267 }
268
269 #[test]
270 fn to_table_empty_shows_no_results() {
271 let table = to_table(&[]);
272 assert_eq!(table, "No results found");
273 }
274
275 #[test]
276 fn to_jsonl_emits_one_line_per_paper() {
277 let out = to_jsonl(&[sample_paper(), sample_paper()]);
278 assert_eq!(out.lines().count(), 2);
279 }
280
281 #[test]
282 fn to_jsonl_lines_are_standalone_json_objects() {
283 let out = to_jsonl(&[sample_paper()]);
284 let line = out.lines().next().unwrap();
285 let v: serde_json::Value = serde_json::from_str(line).unwrap();
286 assert_eq!(v["title"], "Attention Is All You Need");
287 assert_eq!(v["id"], "2301.08745");
288 }
289
290 #[test]
291 fn to_jsonl_of_nothing_is_empty() {
292 assert_eq!(to_jsonl(&[]), "");
293 }
294}