use crate::sources::Paper;
pub fn to_csv(papers: &[Paper]) -> String {
let mut out = String::from("id,title,authors,year,doi,url\n");
for p in papers {
out.push_str(&format!(
"{},{},{},{},{},{}\n",
csv_escape(&p.id),
csv_escape(&p.title),
csv_escape(&p.authors.join(";")),
p.year.map(|y| y.to_string()).unwrap_or_default(),
csv_escape(p.doi.as_deref().unwrap_or("")),
csv_escape(p.url.as_deref().unwrap_or("")),
));
}
out
}
fn csv_escape(field: &str) -> String {
if field.contains(',') || field.contains('"') || field.contains('\n') {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}
pub fn to_bibtex(papers: &[Paper]) -> String {
let mut out = String::new();
for p in papers {
out.push_str(&format!("@article{{{},\n", p.id));
out.push_str(&format!(" title = {{{}}},\n", p.title));
out.push_str(&format!(" author = {{{}}},\n", p.authors.join(" and ")));
if let Some(year) = p.year {
out.push_str(&format!(" year = {{{}}},\n", year));
}
if let Some(ref doi) = p.doi {
out.push_str(&format!(" doi = {{{}}},\n", doi));
}
out.push_str("}\n");
}
out
}
pub fn to_table(papers: &[Paper]) -> String {
if papers.is_empty() {
return "No results found".to_string();
}
let mut out = String::new();
for (i, p) in papers.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&format!("[{}] {}", p.id, p.title));
let mut meta = Vec::new();
if let Some(year) = p.year {
meta.push(year.to_string());
}
if !p.authors.is_empty() {
meta.push(p.authors.join(", "));
}
if !meta.is_empty() {
out.push_str(&format!("\n {}", meta.join(" | ")));
}
}
out.push('\n');
out
}
pub fn to_json(papers: &[Paper]) -> String {
let result = serde_json::json!({
"source": papers.first().map(|p| p.source.as_str()).unwrap_or(""),
"results": papers,
});
serde_json::to_string_pretty(&result).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_paper() -> Paper {
Paper {
id: "2301.08745".to_string(),
title: "Attention Is All You Need".to_string(),
authors: vec!["Alice Smith".to_string(), "Bob Jones".to_string()],
abstract_text: Some("We propose a new architecture.".to_string()),
year: Some(2023),
doi: Some("10.48550/arXiv.2301.08745".to_string()),
url: Some("https://arxiv.org/abs/2301.08745".to_string()),
pdf_url: Some("https://arxiv.org/pdf/2301.08745".to_string()),
venue: Some("arXiv preprint".to_string()),
citations: None,
fields: vec!["cs.CL".to_string()],
open_access: Some(true),
source: "arxiv".to_string(),
}
}
#[test]
fn to_json_returns_valid_json() {
let json = to_json(&[sample_paper()]);
let parsed: serde_json::Value = serde_json::from_str(&json).expect("should be valid JSON");
assert!(parsed.is_object());
}
#[test]
fn to_json_has_source_field() {
let json = to_json(&[sample_paper()]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["source"], "arxiv");
}
#[test]
fn to_json_has_results_array() {
let json = to_json(&[sample_paper()]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let results = v["results"].as_array().expect("results should be array");
assert_eq!(results.len(), 1);
}
#[test]
fn to_json_paper_has_expected_fields() {
let json = to_json(&[sample_paper()]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let paper = &v["results"][0];
assert_eq!(paper["id"], "2301.08745");
assert_eq!(paper["title"], "Attention Is All You Need");
assert_eq!(paper["authors"][0], "Alice Smith");
assert_eq!(paper["year"], 2023);
assert_eq!(paper["doi"], "10.48550/arXiv.2301.08745");
assert_eq!(paper["url"], "https://arxiv.org/abs/2301.08745");
}
#[test]
fn to_json_empty_papers() {
let json = to_json(&[]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let results = v["results"].as_array().expect("results should be array");
assert!(results.is_empty());
}
#[test]
fn to_csv_first_line_is_header() {
let csv = to_csv(&[sample_paper()]);
let first_line = csv.lines().next().unwrap();
assert_eq!(first_line, "id,title,authors,year,doi,url");
}
#[test]
fn to_csv_second_line_contains_id() {
let csv = to_csv(&[sample_paper()]);
let second_line = csv.lines().nth(1).unwrap();
assert!(second_line.starts_with("2301.08745,"));
}
#[test]
fn to_csv_authors_joined_by_semicolon() {
let csv = to_csv(&[sample_paper()]);
let second_line = csv.lines().nth(1).unwrap();
assert!(second_line.contains("Alice Smith;Bob Jones"));
}
#[test]
fn to_csv_quotes_field_with_comma() {
let mut paper = sample_paper();
paper.title = "Attention, Transformers, and You".to_string();
let csv = to_csv(&[paper]);
let second_line = csv.lines().nth(1).unwrap();
assert!(second_line.contains("\"Attention, Transformers, and You\""));
}
#[test]
fn to_csv_empty_returns_header_only() {
let csv = to_csv(&[]);
let lines: Vec<&str> = csv.lines().collect();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "id,title,authors,year,doi,url");
}
#[test]
fn to_bibtex_contains_article_tag() {
let bib = to_bibtex(&[sample_paper()]);
assert!(bib.contains("@article{"));
}
#[test]
fn to_bibtex_contains_title() {
let bib = to_bibtex(&[sample_paper()]);
assert!(bib.contains("title = {Attention Is All You Need}"));
}
#[test]
fn to_bibtex_contains_author() {
let bib = to_bibtex(&[sample_paper()]);
assert!(bib.contains("author = {Alice Smith and Bob Jones}"));
}
#[test]
fn to_bibtex_contains_year() {
let bib = to_bibtex(&[sample_paper()]);
assert!(bib.contains("year = {2023}"));
}
#[test]
fn to_bibtex_contains_doi() {
let bib = to_bibtex(&[sample_paper()]);
assert!(bib.contains("doi = {10.48550/arXiv.2301.08745}"));
}
#[test]
fn to_bibtex_no_doi_when_missing() {
let mut paper = sample_paper();
paper.doi = None;
let bib = to_bibtex(&[paper]);
assert!(!bib.contains("doi ="));
}
#[test]
fn to_bibtex_empty_returns_empty_string() {
let bib = to_bibtex(&[]);
assert!(bib.is_empty());
}
#[test]
fn to_table_contains_title() {
let table = to_table(&[sample_paper()]);
assert!(table.contains("Attention Is All You Need"));
}
#[test]
fn to_table_contains_id() {
let table = to_table(&[sample_paper()]);
assert!(table.contains("2301.08745"));
}
#[test]
fn to_table_contains_year() {
let table = to_table(&[sample_paper()]);
assert!(table.contains("2023"));
}
#[test]
fn to_table_multiple_papers_on_separate_lines() {
let mut p2 = sample_paper();
p2.id = "2312.00001".to_string();
p2.title = "Second Paper".to_string();
let table = to_table(&[sample_paper(), p2]);
assert!(table.contains("Attention Is All You Need"));
assert!(table.contains("Second Paper"));
}
#[test]
fn to_table_empty_shows_no_results() {
let table = to_table(&[]);
assert_eq!(table, "No results found");
}
}