exon_sdf/
io.rs

1// Copyright 2024 WHERE TRUE Technologies.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use tokio::io::{AsyncBufRead, AsyncBufReadExt};
16
17use crate::{record::parse_to_record, Record};
18
19/// A reader for reading records from an SD file.
20pub struct Reader<R> {
21    inner: R,
22}
23
24impl<R> Reader<R>
25where
26    R: AsyncBufRead + Unpin,
27{
28    pub fn new(inner: R) -> Self {
29        Reader { inner }
30    }
31
32    /// Read a single record's bytes from the underlying reader.
33    pub async fn read_record_bytes(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
34        let mut bytes_read = 0;
35        loop {
36            let n = self.inner.read_until(b'\n', buf).await?;
37            bytes_read += n;
38
39            if n == 0 {
40                return Ok(bytes_read);
41            }
42
43            if buf.ends_with(b"$$$$\n") || buf.ends_with(b"$$$$\r\n") {
44                return Ok(bytes_read);
45            }
46        }
47    }
48
49    /// Read record from the underlying reader.
50    pub async fn read_record(&mut self) -> crate::Result<Option<Record>> {
51        let mut buf = Vec::new();
52        let bytes_read = self.read_record_bytes(&mut buf).await?;
53
54        if bytes_read == 0 {
55            return Ok(None);
56        }
57
58        let s = std::str::from_utf8(&buf)?;
59
60        let record = parse_to_record(s)?;
61        Ok(Some(record))
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use crate::record::Data;
68
69    use super::*;
70
71    #[tokio::test]
72    async fn test_read_record() -> crate::Result<()> {
73        let molfile_content = r#"
74    Methane
75    Example
76
772  1  0  0  0  0            999 V2000
78    0.0000    0.0000    0.0000 C   0  0  0  0  0  0
79    0.0000    1.0000    0.0000 H   0  0  0  0  0  0
801  2  1  0  0  0
81M  END
82> <MELTING.POINT>
83-182.5
84
85> <BOILING.POINT>
86-161.5
87
88> <TWO.LINE>
89A
90B
91
92$$$$
93"#
94        .trim();
95
96        let mut reader = Reader::new(std::io::Cursor::new(molfile_content));
97
98        let record = reader.read_record().await?.unwrap();
99
100        assert_eq!(record.header(), "Methane\nExample");
101        assert_eq!(record.data().len(), 3);
102        assert_eq!(record.atom_count(), 2);
103        assert_eq!(record.bond_count(), 1);
104
105        let expected_data = Data::from(vec![
106            ("MELTING.POINT", "-182.5"),
107            ("BOILING.POINT", "-161.5"),
108            // TODO: Fix this test to handle multiline data
109            ("TWO.LINE", "AB"),
110        ]);
111
112        assert_eq!(record.data(), &expected_data);
113
114        Ok(())
115    }
116}