Skip to main content

fastpaper/
output.rs

1use crate::sources::Paper;
2
3/// Format papers as CSV.
4pub 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
28/// Format papers as BibTeX entries.
29pub 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
46/// Format papers as a human-readable table.
47pub 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
72/// Format papers as a JSON search result string.
73pub 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}