1use crate::attributes::Attributes;
4use crate::model::{Record, Strand};
5use std::io::BufRead;
6
7#[derive(Debug)]
8pub enum ParseError {
9 Io(std::io::Error),
10 BadColumnCount {
12 line_no: usize,
13 found: usize,
14 },
15 BadCoordinate {
16 line_no: usize,
17 field: &'static str,
18 },
19}
20
21impl std::fmt::Display for ParseError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 ParseError::Io(e) => write!(f, "I/O error: {e}"),
25 ParseError::BadColumnCount { line_no, found } => {
26 write!(f, "line {line_no}: expected 9 columns, found {found}")
27 }
28 ParseError::BadCoordinate { line_no, field } => {
29 write!(f, "line {line_no}: invalid {field} coordinate")
30 }
31 }
32 }
33}
34
35impl std::error::Error for ParseError {}
36
37impl From<std::io::Error> for ParseError {
38 fn from(e: std::io::Error) -> Self {
39 ParseError::Io(e)
40 }
41}
42
43pub struct GffReader<R: BufRead> {
46 inner: R,
47 line_no: usize,
48 buf: Vec<u8>,
49 in_fasta: bool,
50}
51
52impl<R: BufRead> GffReader<R> {
53 pub fn new(inner: R) -> Self {
54 Self {
55 inner,
56 line_no: 0,
57 buf: Vec::new(),
58 in_fasta: false,
59 }
60 }
61
62 pub fn next_record(&mut self) -> Result<Option<Record>, ParseError> {
68 loop {
69 self.buf.clear();
70 let n = self.inner.read_until(b'\n', &mut self.buf)?;
71 if n == 0 {
72 return Ok(None);
73 }
74 self.line_no += 1;
75 let raw = String::from_utf8_lossy(&self.buf);
76 let line = raw.trim_end_matches(['\n', '\r']);
77
78 if self.in_fasta {
79 continue;
80 }
81 if line.is_empty() {
82 continue;
83 }
84 if line.starts_with('#') {
85 if line.trim_end() == "##FASTA" {
87 self.in_fasta = true;
88 }
89 continue;
90 }
91 if line.starts_with('>') {
93 self.in_fasta = true;
94 continue;
95 }
96
97 return Ok(Some(self.parse_line(line)?));
98 }
99 }
100
101 fn parse_line(&self, line: &str) -> Result<Record, ParseError> {
102 let cols: Vec<&str> = line.splitn(9, '\t').collect();
105 if cols.len() < 9 {
106 return Err(ParseError::BadColumnCount {
107 line_no: self.line_no,
108 found: cols.len(),
109 });
110 }
111
112 let start = cols[3]
113 .parse::<u64>()
114 .map_err(|_| ParseError::BadCoordinate {
115 line_no: self.line_no,
116 field: "start",
117 })?;
118 let end = cols[4]
119 .parse::<u64>()
120 .map_err(|_| ParseError::BadCoordinate {
121 line_no: self.line_no,
122 field: "end",
123 })?;
124
125 Ok(Record {
126 seqid: cols[0].to_string(),
127 source: cols[1].to_string(),
128 feature_type: cols[2].to_string(),
129 start,
130 end,
131 score: cols[5].to_string(),
132 strand: Strand::parse(cols[6]),
133 phase: cols[7].to_string(),
134 attributes: Attributes::parse_gff3(cols[8]),
135 })
136 }
137}
138
139pub fn read_all<R: BufRead>(reader: R) -> Result<Vec<Record>, ParseError> {
143 let mut r = GffReader::new(reader);
144 let mut out = Vec::new();
145 while let Some(rec) = r.next_record()? {
146 out.push(rec);
147 }
148 Ok(out)
149}
150
151pub fn read_all_sanitize<R, F>(
156 reader: R,
157 mut on_skip: F,
158) -> Result<(Vec<Record>, usize), ParseError>
159where
160 R: BufRead,
161 F: FnMut(&ParseError),
162{
163 let mut r = GffReader::new(reader);
164 let mut out = Vec::new();
165 let mut skipped = 0;
166 loop {
167 match r.next_record() {
168 Ok(Some(rec)) => out.push(rec),
169 Ok(None) => return Ok((out, skipped)),
170 Err(ParseError::Io(e)) => return Err(ParseError::Io(e)),
171 Err(e) => {
172 skipped += 1;
173 on_skip(&e);
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::io::Cursor;
183
184 #[test]
185 fn reads_records_skips_comments_and_fasta() {
186 let data = "\
187##gff-version 3
188chr1\tEnsembl\tgene\t1\t100\t.\t+\t.\tID=g1
189chr1\tEnsembl\tmRNA\t1\t100\t.\t+\t.\tID=t1;Parent=g1
190
191##FASTA
192>chr1
193ACGT
194";
195 let recs = read_all(Cursor::new(data)).unwrap();
196 assert_eq!(recs.len(), 2);
197 assert_eq!(recs[0].id(), Some("g1"));
198 assert_eq!(recs[1].parent(), Some("g1"));
199 }
200
201 #[test]
202 fn tolerates_non_utf8_bytes() {
203 let mut data: Vec<u8> = b"chr1\tsrc\tgene\t1\t100\t.\t+\t.\tID=g1;Note=caf".to_vec();
205 data.push(0xE9);
206 data.push(b'\n');
207 let recs = read_all(Cursor::new(data)).unwrap();
208 assert_eq!(recs.len(), 1);
209 assert_eq!(recs[0].id(), Some("g1"));
210 }
211
212 #[test]
213 fn too_few_columns_reports_line_and_count() {
214 let data = b"chr1\tsrc\tgene\t1\t100\n".to_vec();
215 match read_all(Cursor::new(data)) {
216 Err(ParseError::BadColumnCount { line_no, found }) => {
217 assert_eq!(line_no, 1);
218 assert_eq!(found, 5);
219 }
220 other => panic!("expected BadColumnCount, got {other:?}"),
221 }
222 }
223
224 #[test]
225 fn sanitize_skips_bad_records_and_continues() {
226 let data = b"\
227chr1\tsrc\tgene\t1\t100\t.\t+\t.\tID=g1
228bad\ttoo\tfew
229chr1\tsrc\tmRNA\t1\t100\t.\t+\t.\tID=t1;Parent=g1
230"
231 .to_vec();
232 let mut diagnostics = Vec::new();
233 let (recs, skipped) =
234 read_all_sanitize(Cursor::new(data), |e| diagnostics.push(e.to_string())).unwrap();
235 assert_eq!(skipped, 1);
236 assert_eq!(recs.len(), 2);
237 assert!(diagnostics[0].contains("line 2: expected 9 columns"));
238 }
239}