1use crate::SourceReader;
2use crate::SourceRow;
3use anyhow::Result;
4use apif_source_error::SourceError;
5use std::io::{BufReader, Read, Seek};
6
7pub struct CsvReader<R> {
8 reader: csv::Reader<BufReader<R>>,
9 headers: Vec<String>,
10 _delimiter: u8,
11 row_number: usize,
12 finished: bool,
13}
14
15impl<R: Read> CsvReader<R> {
16 pub fn new(reader: BufReader<R>, delimiter: u8) -> Result<Self> {
17 let mut csv_reader = csv::ReaderBuilder::new()
18 .delimiter(delimiter)
19 .comment(Some(b'#'))
20 .has_headers(true)
21 .flexible(false)
22 .from_reader(reader);
23
24 let headers = csv_reader
25 .headers()
26 .map_err(|e| anyhow::anyhow!("failed to read CSV header: {e}"))?
27 .iter()
28 .map(|h| h.to_string())
29 .collect::<Vec<_>>();
30
31 if headers.is_empty() {
32 return Err(SourceError::EmptyFile("csv".into()).into());
33 }
34
35 let mut seen = std::collections::HashSet::new();
36 for col in &headers {
37 if !seen.insert(col.as_str()) {
38 return Err(SourceError::DuplicateColumn(col.clone()).into());
39 }
40 }
41
42 Ok(Self {
43 reader: csv_reader,
44 headers,
45 _delimiter: delimiter,
46 row_number: 1,
47 finished: false,
48 })
49 }
50}
51
52impl<R: Read + Send> SourceReader for CsvReader<R> {
53 fn next_row(&mut self) -> Result<Option<SourceRow>> {
54 if self.finished {
55 return Ok(None);
56 }
57
58 if let Some(result) = self.reader.records().next() {
59 self.row_number += 1;
60 let record = match result {
61 Ok(r) => r,
62 Err(e) => {
63 return Err(anyhow::anyhow!("CSV error at row {}: {e}", self.row_number));
64 }
65 };
66
67 let values: Vec<String> = record.iter().map(|f| f.to_string()).collect();
68
69 if values.len() != self.headers.len() {
70 return Err(SourceError::FieldCountMismatch {
71 row: self.row_number,
72 fields: values.len(),
73 expected: self.headers.len(),
74 }
75 .into());
76 }
77
78 return Ok(Some(SourceRow::new(&self.headers, values)));
79 }
80
81 self.finished = true;
82 Ok(None)
83 }
84
85 fn headers(&self) -> &[String] {
86 &self.headers
87 }
88
89 fn reset(&mut self) -> Result<()> {
90 Ok(())
91 }
92}
93
94impl<R: Read + Seek + Send> CsvReader<R> {
95 pub fn reset_seekable(&mut self) -> Result<()> {
96 Err(anyhow::anyhow!(
99 "reset_seekable not supported with csv crate reader"
100 ))
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::io::Cursor;
108
109 fn cursor(data: &str) -> BufReader<Cursor<&str>> {
110 BufReader::new(Cursor::new(data))
111 }
112
113 #[test]
114 fn csv_reads_basic_rows() {
115 let data = "id,name,age\n1,Alice,30\n2,Bob,25\n";
116 let mut reader = CsvReader::new(cursor(data), b',').unwrap();
117 assert_eq!(reader.headers(), &["id", "name", "age"]);
118
119 let row1 = reader.next_row().unwrap().unwrap();
120 assert_eq!(row1.get("id"), Some("1"));
121 assert_eq!(row1.get("name"), Some("Alice"));
122 assert_eq!(row1.get("age"), Some("30"));
123
124 let row2 = reader.next_row().unwrap().unwrap();
125 assert_eq!(row2.get("name"), Some("Bob"));
126
127 assert!(reader.next_row().unwrap().is_none());
128 }
129
130 #[test]
131 fn csv_handles_quoted_fields() {
132 let data = "id,name\n1,\"Smith, John\"\n2,\"O\"\"Brien\"\n";
133 let mut reader = CsvReader::new(cursor(data), b',').unwrap();
134
135 let row1 = reader.next_row().unwrap().unwrap();
136 assert_eq!(row1.get("name"), Some("Smith, John"));
137
138 let row2 = reader.next_row().unwrap().unwrap();
139 assert_eq!(row2.get("name"), Some("O\"Brien"));
140 }
141
142 #[test]
143 fn csv_skips_comments_and_blank_lines() {
144 let data = "id,val\n# comment\n\n1,hello\n\n2,world\n";
145 let mut reader = CsvReader::new(cursor(data), b',').unwrap();
146 let row1 = reader.next_row().unwrap().unwrap();
147 assert_eq!(row1.get("val"), Some("hello"));
148 let row2 = reader.next_row().unwrap().unwrap();
149 assert_eq!(row2.get("val"), Some("world"));
150 assert!(reader.next_row().unwrap().is_none());
151 }
152
153 #[test]
154 fn csv_empty_file_errors() {
155 let data = "";
156 let result = CsvReader::new(cursor(data), b',');
157 assert!(result.is_err());
158 }
159
160 #[test]
161 fn csv_duplicate_column_errors() {
162 let data = "id,id,value\n1,2,3\n";
163 let result = CsvReader::new(cursor(data), b',');
164 assert!(result.is_err());
165 }
166
167 #[test]
168 fn csv_field_count_mismatch_errors() {
169 let data = "id,name\n1,Alice,extra\n";
170 let mut reader = CsvReader::new(cursor(data), b',').unwrap();
171 let result = reader.next_row();
172 assert!(result.is_err());
173 }
174
175 #[test]
176 fn csv_custom_delimiter() {
177 let data = "id;name;age\n1;Alice;30\n";
178 let mut reader = CsvReader::new(cursor(data), b';').unwrap();
179 let row = reader.next_row().unwrap().unwrap();
180 assert_eq!(row.get("name"), Some("Alice"));
181 }
182}