1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! A line within a chain file.

use std::str::FromStr;

use crate::record::alignment_data;
use crate::record::alignment_data::AlignmentDataRecord;
use crate::record::header;
use crate::record::header::HeaderRecord;
use crate::record::header::HEADER_PREFIX;

/// An error associated with parsing the chain file.
#[derive(Debug)]
pub enum ParseError {
    /// An invalid header record.
    InvalidHeaderRecord(header::ParseError, String),
    /// An invalid alignment data record.
    InvalidAlignmentDataRecord(alignment_data::ParseError, String),
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::InvalidHeaderRecord(err, line) => {
                write!(f, "invalid header record: {}\n\nline: {}", err, line)
            }
            ParseError::InvalidAlignmentDataRecord(err, line) => {
                write!(
                    f,
                    "invalid alignment data record: {}\n\nline: {}",
                    err, line
                )
            }
        }
    }
}

impl std::error::Error for ParseError {}

/// A line within a chain file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Line {
    /// An empty line.
    Empty,
    /// A header line.
    Header(HeaderRecord),
    /// An alignment data line.
    AlignmentData(AlignmentDataRecord),
}

impl std::fmt::Display for Line {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Line::Empty => write!(f, ""),
            Line::Header(record) => write!(f, "{}", record),
            Line::AlignmentData(record) => write!(f, "{}", record),
        }
    }
}

impl FromStr for Line {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            Ok(Self::Empty)
        } else if s.starts_with(HEADER_PREFIX) {
            s.parse::<HeaderRecord>()
                .map(Line::Header)
                .map_err(|e| ParseError::InvalidHeaderRecord(e, s.into()))
        } else {
            s.parse::<AlignmentDataRecord>()
                .map(Line::AlignmentData)
                .map_err(|e| ParseError::InvalidAlignmentDataRecord(e, s.into()))
        }
    }
}

#[cfg(test)]
pub mod tests {
    use crate::record::alignment_data::AlignmentDataRecordType;

    use super::*;

    #[test]
    pub fn test_valid_header_line() -> Result<(), Box<dyn std::error::Error>> {
        let line = "chain 0 seq0 2 + 0 2 seq0 2 - 0 2 1".parse::<Line>()?;
        assert!(matches!(line, Line::Header(_)));
        Ok(())
    }

    #[test]
    pub fn test_valid_nonterminating_alignment_data_line() -> Result<(), Box<dyn std::error::Error>>
    {
        let line = "9\t0\t1".parse::<Line>()?;
        assert!(matches!(line, Line::AlignmentData(_)));
        if let Line::AlignmentData(record) = line {
            assert_eq!(
                *record.record_type(),
                AlignmentDataRecordType::NonTerminating
            );
        }
        Ok(())
    }

    #[test]
    pub fn test_valid_terminating_alignment_data_line() -> Result<(), Box<dyn std::error::Error>> {
        let line = "9".parse::<Line>()?;
        assert!(matches!(line, Line::AlignmentData(_)));
        if let Line::AlignmentData(record) = line {
            assert_eq!(*record.record_type(), AlignmentDataRecordType::Terminating);
        }
        Ok(())
    }

    #[test]
    pub fn test_invalid_header_line() -> Result<(), Box<dyn std::error::Error>> {
        let err = "chain 0 seq0 2 + 0 2 seq0 2 - 0 2 ?"
            .parse::<Line>()
            .unwrap_err();
        assert_eq!(
            err.to_string(),
            "invalid header record: invalid id: invalid digit \
             found in string\n\nline: chain 0 seq0 2 + 0 2 seq0 2 - 0 2 ?"
        );
        Ok(())
    }

    #[test]
    pub fn test_invalid_alignment_data_line() -> Result<(), Box<dyn std::error::Error>> {
        let err = "9\t1".parse::<Line>().unwrap_err();
        assert_eq!(
            err.to_string(),
            "invalid alignment data record: invalid number of fields in alignment data: \
            expected 3 (non-terminating) or 1 (terminating) fields, found 2 fields\n\n\
            line: 9\t1"
        );
        Ok(())
    }
}