Skip to main content

apif_source_row/
tsv.rs

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