Skip to main content

chainfile/
line.rs

1//! A line within a chain file.
2
3use thiserror::Error;
4
5use crate::alignment::section::data;
6use crate::alignment::section::data::Record as AlignmentDataRecord;
7use crate::alignment::section::header;
8use crate::alignment::section::header::HEADER_PREFIX;
9use crate::alignment::section::header::Record as HeaderRecord;
10
11/// An error associated with parsing the chain file.
12#[derive(Debug, Error)]
13pub enum Error {
14    /// An invalid header record.
15    #[error("invalid header record: {inner}\n\nline: `{line}`")]
16    InvalidHeaderRecord {
17        /// The inner error.
18        inner: header::Error,
19
20        /// The literal line.
21        line: String,
22    },
23
24    /// An invalid alignment data record.
25    #[error("invalid alignment data record: {inner}\n\nline: `{line}`")]
26    InvalidAlignmentDataRecord {
27        /// The inner error.
28        inner: data::Error,
29
30        /// The literal line.
31        line: String,
32    },
33}
34
35/// A line within a chain file.
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum Line {
38    /// An empty line.
39    Empty,
40
41    /// A header line.
42    Header(HeaderRecord),
43
44    /// An alignment data line.
45    AlignmentData(AlignmentDataRecord),
46}
47
48impl Line {
49    /// Attempts to return a reference to the inner header record.
50    pub fn as_header(&self) -> Option<&HeaderRecord> {
51        match self {
52            Line::Header(record) => Some(record),
53            _ => None,
54        }
55    }
56
57    /// Consumes `self` and attempts to return a reference to the inner header
58    /// record.
59    pub fn into_header(self) -> Option<HeaderRecord> {
60        match self {
61            Line::Header(record) => Some(record),
62            _ => None,
63        }
64    }
65
66    /// Attempts to return a reference to the inner alignment data record.
67    pub fn as_alignment_data(&self) -> Option<&AlignmentDataRecord> {
68        match self {
69            Line::AlignmentData(record) => Some(record),
70            _ => None,
71        }
72    }
73
74    /// Consumes `self` and attempts to return a reference to the inner
75    /// alignment data record.
76    pub fn into_alignment_data_record(self) -> Option<AlignmentDataRecord> {
77        match self {
78            Line::AlignmentData(record) => Some(record),
79            _ => None,
80        }
81    }
82}
83
84impl std::fmt::Display for Line {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            Line::Empty => write!(f, ""),
88            Line::Header(record) => write!(f, "{record}"),
89            Line::AlignmentData(record) => write!(f, "{record}"),
90        }
91    }
92}
93
94impl std::str::FromStr for Line {
95    type Err = Error;
96
97    fn from_str(s: &str) -> Result<Self, Self::Err> {
98        if s.is_empty() {
99            Ok(Self::Empty)
100        } else if s.starts_with(HEADER_PREFIX) {
101            s.parse::<HeaderRecord>()
102                .map(Line::Header)
103                .map_err(|err| Error::InvalidHeaderRecord {
104                    inner: err,
105                    line: s.into(),
106                })
107        } else {
108            s.parse::<AlignmentDataRecord>()
109                .map(Line::AlignmentData)
110                .map_err(|err| Error::InvalidAlignmentDataRecord {
111                    inner: err,
112                    line: s.into(),
113                })
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::alignment::section::data::record::Kind;
122
123    #[test]
124    pub fn valid_header_line() {
125        let line = "chain 0 seq0 2 + 0 2 seq0 2 - 0 2 1"
126            .parse::<Line>()
127            .unwrap();
128
129        let record = line.into_header().unwrap();
130
131        assert_eq!(record.score(), 0);
132        assert_eq!(record.id(), 1);
133    }
134
135    #[test]
136    pub fn valid_nonterminating_alignment_data_line() {
137        let line = "9\t0\t1".parse::<Line>().unwrap();
138        let record = line.into_alignment_data_record().unwrap();
139
140        assert_eq!(record.size(), 9);
141        assert_eq!(record.dt().unwrap(), 0);
142        assert_eq!(record.dq().unwrap(), 1);
143        assert_eq!(record.kind(), Kind::NonTerminating);
144    }
145
146    #[test]
147    pub fn valid_terminating_alignment_data_line() {
148        let line = "9".parse::<Line>().unwrap();
149        let record = line.into_alignment_data_record().unwrap();
150
151        assert_eq!(record.size(), 9);
152        assert!(record.dt().is_none());
153        assert!(record.dq().is_none());
154        assert_eq!(record.kind(), Kind::Terminating);
155    }
156
157    #[test]
158    pub fn invalid_header_line() {
159        let err = "chain 0 seq0 2 + 0 2 seq0 2 - 0 2 ?"
160            .parse::<Line>()
161            .unwrap_err();
162
163        assert_eq!(
164            err.to_string(),
165            "invalid header record: parse error: invalid id: invalid digit found in \
166             string\n\nline: `chain 0 seq0 2 + 0 2 seq0 2 - 0 2 ?`"
167        );
168    }
169
170    #[test]
171    pub fn invalid_alignment_data_line() {
172        let err = "9\t1".parse::<Line>().unwrap_err();
173
174        assert_eq!(
175            err.to_string(),
176            "invalid alignment data record: parse error: invalid number of fields in alignment \
177             data: expected 3 (non-terminating) or 1 (terminating) fields, found 2 \
178             fields\n\nline: `9\t1`"
179        );
180    }
181}