physis/
exl.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use crate::{ByteBuffer, ByteSpan};
5use std::io::{BufRead, BufReader, BufWriter, Cursor, Write};
6
7/// Represents an Excel List.
8pub struct EXL {
9    /// The version of the list.
10    pub version: i32,
11
12    /// The entries of the list.
13    pub entries: Vec<(String, i32)>,
14}
15
16impl EXL {
17    /// Initializes `EXL` from an existing list.
18    pub fn from_existing(buffer: ByteSpan) -> Option<EXL> {
19        let mut exl = Self {
20            version: 0,
21            entries: Vec::new(),
22        };
23
24        let cursor = Cursor::new(buffer);
25        let reader = BufReader::new(cursor);
26
27        for line in reader.lines().map_while(Result::ok) {
28            if let Some((name, value)) = line.split_once(',') {
29                if let Ok(parsed_value) = value.parse() {
30                    if name == "EXLT" {
31                        exl.version = parsed_value;
32                    } else if !name.starts_with('#') {
33                        // Ignore rows with comments
34                        exl.entries.push((name.to_string(), parsed_value));
35                    }
36                }
37            }
38        }
39
40        Some(exl)
41    }
42
43    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
44        let mut buffer = ByteBuffer::new();
45
46        {
47            let cursor = Cursor::new(&mut buffer);
48            let mut writer = BufWriter::new(cursor);
49
50            writer
51                .write_all(format!("EXLT,{}", self.version).as_ref())
52                .ok()?;
53
54            for (key, value) in &self.entries {
55                writer.write_all(format!("\n{key},{value}").as_ref()).ok()?;
56            }
57        }
58
59        Some(buffer)
60    }
61
62    /// Checks whether or not the list contains a key.
63    ///
64    /// # Example
65    ///
66    /// ```
67    /// # use std::fs::read;
68    /// # use std::path::PathBuf;
69    /// # use physis::exl::EXL;
70    /// # let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
71    /// # d.push("resources/tests");
72    /// # d.push("test.exl");
73    /// # let exl_file = read(d).unwrap();
74    /// let exl = EXL::from_existing(&exl_file).unwrap();
75    /// exl.contains("Foo");
76    /// ```
77    pub fn contains(&self, key: &str) -> bool {
78        self.entries.iter().any(|t| t.0 == key)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use std::fs::read;
85    use std::path::PathBuf;
86
87    use super::*;
88
89    fn common_setup() -> EXL {
90        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
91        d.push("resources/tests");
92        d.push("test.exl");
93
94        EXL::from_existing(&read(d).unwrap()).unwrap()
95    }
96
97    #[test]
98    fn version_parsing() {
99        let exl = common_setup();
100
101        assert_eq!(exl.version, 2);
102    }
103
104    #[test]
105    fn contains() {
106        let exl = common_setup();
107
108        assert!(exl.contains("Foo"));
109
110        // should be case-sensitive
111        assert!(!exl.contains("foo"));
112    }
113
114    #[test]
115    fn test_write() {
116        let existing_exl = common_setup();
117
118        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
119        d.push("resources/tests");
120        d.push("test.exl");
121
122        let exl = read(d).unwrap();
123
124        let mut out = std::io::stdout();
125        out.write_all(&existing_exl.write_to_buffer().unwrap())
126            .unwrap();
127        out.flush().unwrap();
128
129        assert_eq!(existing_exl.write_to_buffer().unwrap(), exl);
130    }
131
132    #[test]
133    fn test_invalid() {
134        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
135        d.push("resources/tests");
136        d.push("random");
137
138        // Feeding it invalid data should not panic
139        EXL::from_existing(&read(d).unwrap());
140    }
141}