1use 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#[derive(Debug, Error)]
13pub enum Error {
14 #[error("invalid header record: {inner}\n\nline: `{line}`")]
16 InvalidHeaderRecord {
17 inner: header::Error,
19
20 line: String,
22 },
23
24 #[error("invalid alignment data record: {inner}\n\nline: `{line}`")]
26 InvalidAlignmentDataRecord {
27 inner: data::Error,
29
30 line: String,
32 },
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum Line {
38 Empty,
40
41 Header(HeaderRecord),
43
44 AlignmentData(AlignmentDataRecord),
46}
47
48impl Line {
49 pub fn as_header(&self) -> Option<&HeaderRecord> {
51 match self {
52 Line::Header(record) => Some(record),
53 _ => None,
54 }
55 }
56
57 pub fn into_header(self) -> Option<HeaderRecord> {
60 match self {
61 Line::Header(record) => Some(record),
62 _ => None,
63 }
64 }
65
66 pub fn as_alignment_data(&self) -> Option<&AlignmentDataRecord> {
68 match self {
69 Line::AlignmentData(record) => Some(record),
70 _ => None,
71 }
72 }
73
74 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}