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
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 fn sample_paper() -> Paper {
86 Paper {
87 id: "2301.08745".to_string(),
88 title: "Attention Is All You Need".to_string(),
89 authors: vec!["Alice Smith".to_string(), "Bob Jones".to_string()],
90 abstract_text: Some("We propose a new architecture.".to_string()),
91 year: Some(2023),
92 doi: Some("10.48550/arXiv.2301.08745".to_string()),
93 url: Some("https://arxiv.org/abs/2301.08745".to_string()),
94 pdf_url: Some("https://arxiv.org/pdf/2301.08745".to_string()),
95 venue: Some("arXiv preprint".to_string()),
96 citations: None,
97 fields: vec!["cs.CL".to_string()],
98 open_access: Some(true),
99 source: "arxiv".to_string(),
100 }
101 }
102
103 #[test]
104 fn to_json_returns_valid_json() {
105 let json = to_json(&[sample_paper()]);
106 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should be valid JSON");
107 assert!(parsed.is_object());
108 }
109
110 #[test]
111 fn to_json_has_source_field() {
112 let json = to_json(&[sample_paper()]);
113 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
114 assert_eq!(v["source"], "arxiv");
115 }
116
117 #[test]
118 fn to_json_has_results_array() {
119 let json = to_json(&[sample_paper()]);
120 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
121 let results = v["results"].as_array().expect("results should be array");
122 assert_eq!(results.len(), 1);
123 }
124
125 #[test]
126 fn to_json_paper_has_expected_fields() {
127 let json = to_json(&[sample_paper()]);
128 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
129 let paper = &v["results"][0];
130 assert_eq!(paper["id"], "2301.08745");
131 assert_eq!(paper["title"], "Attention Is All You Need");
132 assert_eq!(paper["authors"][0], "Alice Smith");
133 assert_eq!(paper["year"], 2023);
134 assert_eq!(paper["doi"], "10.48550/arXiv.2301.08745");
135 assert_eq!(paper["url"], "https://arxiv.org/abs/2301.08745");
136 }
137
138 #[test]
139 fn to_json_empty_papers() {
140 let json = to_json(&[]);
141 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
142 let results = v["results"].as_array().expect("results should be array");
143 assert!(results.is_empty());
144 }
145
146 #[test]
147 fn to_csv_first_line_is_header() {
148 let csv = to_csv(&[sample_paper()]);
149 let first_line = csv.lines().next().unwrap();
150 assert_eq!(first_line, "id,title,authors,year,doi,url");
151 }
152
153 #[test]
154 fn to_csv_second_line_contains_id() {
155 let csv = to_csv(&[sample_paper()]);
156 let second_line = csv.lines().nth(1).unwrap();
157 assert!(second_line.starts_with("2301.08745,"));
158 }
159
160 #[test]
161 fn to_csv_authors_joined_by_semicolon() {
162 let csv = to_csv(&[sample_paper()]);
163 let second_line = csv.lines().nth(1).unwrap();
164 assert!(second_line.contains("Alice Smith;Bob Jones"));
165 }
166
167 #[test]
168 fn to_csv_quotes_field_with_comma() {
169 let mut paper = sample_paper();
170 paper.title = "Attention, Transformers, and You".to_string();
171 let csv = to_csv(&[paper]);
172 let second_line = csv.lines().nth(1).unwrap();
173 assert!(second_line.contains("\"Attention, Transformers, and You\""));
174 }
175
176 #[test]
177 fn to_csv_empty_returns_header_only() {
178 let csv = to_csv(&[]);
179 let lines: Vec<&str> = csv.lines().collect();
180 assert_eq!(lines.len(), 1);
181 assert_eq!(lines[0], "id,title,authors,year,doi,url");
182 }
183
184 #[test]
185 fn to_bibtex_contains_article_tag() {
186 let bib = to_bibtex(&[sample_paper()]);
187 assert!(bib.contains("@article{"));
188 }
189
190 #[test]
191 fn to_bibtex_contains_title() {
192 let bib = to_bibtex(&[sample_paper()]);
193 assert!(bib.contains("title = {Attention Is All You Need}"));
194 }
195
196 #[test]
197 fn to_bibtex_contains_author() {
198 let bib = to_bibtex(&[sample_paper()]);
199 assert!(bib.contains("author = {Alice Smith and Bob Jones}"));
200 }
201
202 #[test]
203 fn to_bibtex_contains_year() {
204 let bib = to_bibtex(&[sample_paper()]);
205 assert!(bib.contains("year = {2023}"));
206 }
207
208 #[test]
209 fn to_bibtex_contains_doi() {
210 let bib = to_bibtex(&[sample_paper()]);
211 assert!(bib.contains("doi = {10.48550/arXiv.2301.08745}"));
212 }
213
214 #[test]
215 fn to_bibtex_no_doi_when_missing() {
216 let mut paper = sample_paper();
217 paper.doi = None;
218 let bib = to_bibtex(&[paper]);
219 assert!(!bib.contains("doi ="));
220 }
221
222 #[test]
223 fn to_bibtex_empty_returns_empty_string() {
224 let bib = to_bibtex(&[]);
225 assert!(bib.is_empty());
226 }
227
228 #[test]
229 fn to_table_contains_title() {
230 let table = to_table(&[sample_paper()]);
231 assert!(table.contains("Attention Is All You Need"));
232 }
233
234 #[test]
235 fn to_table_contains_id() {
236 let table = to_table(&[sample_paper()]);
237 assert!(table.contains("2301.08745"));
238 }
239
240 #[test]
241 fn to_table_contains_year() {
242 let table = to_table(&[sample_paper()]);
243 assert!(table.contains("2023"));
244 }
245
246 #[test]
247 fn to_table_multiple_papers_on_separate_lines() {
248 let mut p2 = sample_paper();
249 p2.id = "2312.00001".to_string();
250 p2.title = "Second Paper".to_string();
251 let table = to_table(&[sample_paper(), p2]);
252 assert!(table.contains("Attention Is All You Need"));
253 assert!(table.contains("Second Paper"));
254 }
255
256 #[test]
257 fn to_table_empty_shows_no_results() {
258 let table = to_table(&[]);
259 assert_eq!(table, "No results found");
260 }
261}