Skip to main content

harper_core/weirpack/
mod.rs

1//! See [our main documentation](https://writewithharper.com/docs/weir#Weirpacks) on Weir and the Weirpack format.
2
3use std::io::{Read, Write};
4use std::path::Path;
5
6use hashbrown::HashMap;
7use zip::write::FileOptions;
8use zip::{CompressionMethod, ZipArchive, ZipWriter};
9
10use crate::linting::LintGroup;
11use crate::spell::MutableDictionary;
12use crate::weir::{TestResult, WeirLinter};
13
14mod error;
15mod manifest;
16
17pub use error::Error;
18pub use manifest::WeirpackManifest;
19
20/// A Weirpack, which carries within itself one or more rules to be used for grammar checking.
21/// These rules are written in Weir.
22#[derive(Debug, Clone, Default)]
23pub struct Weirpack {
24    pub rules: HashMap<String, String>,
25    /// The `dictionary.dict` file, if it exists.
26    pub dictionary: Option<String>,
27    /// The `annotations.json` file, if it exists.
28    pub annotations: Option<String>,
29    pub manifest: WeirpackManifest,
30}
31
32impl Weirpack {
33    /// Create an empty Weirpack.
34    pub fn new(manifest: WeirpackManifest) -> Self {
35        Self {
36            rules: HashMap::new(),
37            annotations: None,
38            dictionary: None,
39            manifest,
40        }
41    }
42
43    /// Add a rule to this Weirpack. Does not compile to test the rule.
44    pub fn add_rule(&mut self, name: impl Into<String>, rule: impl Into<String>) -> Option<String> {
45        self.rules.insert(name.into(), rule.into())
46    }
47
48    /// Remove a rule from this Weirpack.
49    pub fn remove_rule(&mut self, name: &str) -> Option<String> {
50        self.rules.remove(name)
51    }
52
53    /// Run all the tests within all the Weir rules in this Weirpack.
54    pub fn run_tests(&self) -> Result<HashMap<String, Vec<TestResult>>, Error> {
55        let mut failures = HashMap::new();
56
57        for (name, rule) in &self.rules {
58            let mut linter = WeirLinter::new(rule)?;
59            let failing_tests = linter.run_tests();
60            if !failing_tests.is_empty() {
61                failures.insert(name.to_string(), failing_tests);
62            }
63        }
64
65        Ok(failures)
66    }
67
68    /// Parse and optimize the Weir rules in the pack, converting the set into a single [`LintGroup`].
69    /// Does not run tests.
70    pub fn to_lint_group(&self) -> Result<LintGroup, Error> {
71        let mut group = LintGroup::default();
72
73        for (name, rule) in &self.rules {
74            let linter = WeirLinter::new(rule)?;
75            group.add_chunk_expr_linter(name, linter);
76            group.config.set_rule_enabled(name, true);
77        }
78
79        Ok(group)
80    }
81
82    /// Load a Weirpack from bytes.
83    pub fn from_reader(mut reader: impl Read) -> Result<Self, Error> {
84        let mut bytes = Vec::new();
85        reader.read_to_end(&mut bytes)?;
86        Self::from_bytes(&bytes)
87    }
88
89    /// Write the Weirpack to bytes.
90    pub fn write_to(&self, mut writer: impl Write) -> Result<(), Error> {
91        let bytes = self.to_bytes()?;
92        writer.write_all(&bytes)?;
93        Ok(())
94    }
95
96    /// Loads the dictionary that may or may not be contained within the Weirpack.
97    ///
98    /// The dictionary is in the Rune format and thus is composed of two files, `annotations.json`
99    /// and `dictionary.dict`.
100    ///
101    /// Returns `None` if the relevant files are not present in the Weirpack.
102    pub fn load_dictionary(&self) -> Result<Option<MutableDictionary>, Error> {
103        if let Some(dict) = &self.dictionary
104            && let Some(annot) = &self.annotations
105        {
106            Ok(Some(
107                MutableDictionary::from_rune_files(dict, annot)
108                    .map_err(|_| Error::InvalidDictionaryFormat)?,
109            ))
110        } else {
111            Ok(None)
112        }
113    }
114
115    /// Load a Weirpack from bytes.
116    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
117        let cursor = std::io::Cursor::new(bytes);
118        let mut archive = ZipArchive::new(cursor)?;
119
120        let mut manifest = None;
121        let mut rules = HashMap::new();
122        let mut dictionary = None;
123        let mut annotations = None;
124
125        for i in 0..archive.len() {
126            let mut file = archive.by_index(i)?;
127            if file.is_dir() {
128                continue;
129            }
130
131            let name = file.name().to_string();
132            if name == "manifest.json" {
133                if manifest.is_some() {
134                    return Err(Error::DuplicateManifest("manifest.json"));
135                }
136                let manifest_data = WeirpackManifest::from_reader(&mut file)?;
137                manifest = Some(manifest_data);
138                continue;
139            }
140
141            if name.ends_with(".weir") {
142                let path = Path::new(&name);
143                let file_name = path
144                    .file_name()
145                    .and_then(|segment| segment.to_str())
146                    .ok_or_else(|| Error::InvalidRuleFileName(name.clone()))?;
147                let rule_name = Path::new(file_name)
148                    .file_stem()
149                    .and_then(|stem| stem.to_str())
150                    .ok_or_else(|| Error::InvalidRuleFileName(name.clone()))?;
151
152                let mut contents = String::new();
153                file.read_to_string(&mut contents)?;
154                rules.insert(rule_name.to_string(), contents);
155            } else if name == "dictionary.dict" {
156                let mut contents = String::new();
157                file.read_to_string(&mut contents)?;
158                dictionary = Some(contents);
159            } else if name == "annotations.json" {
160                let mut contents = String::new();
161                file.read_to_string(&mut contents)?;
162                annotations = Some(contents);
163            }
164        }
165
166        let manifest = manifest.ok_or(Error::MissingManifest("manifest.json"))?;
167
168        Ok(Self {
169            rules,
170            manifest,
171            annotations,
172            dictionary,
173        })
174    }
175
176    /// Write a Weirpack into bytes.
177    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
178        let mut zip = ZipWriter::new(std::io::Cursor::new(Vec::new()));
179        let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated);
180
181        let mut manifest_bytes = Vec::new();
182        self.manifest.write_to(&mut manifest_bytes)?;
183        zip.start_file("manifest.json", options)?;
184        zip.write_all(&manifest_bytes)?;
185
186        if let Some(annot) = &self.annotations {
187            zip.start_file("annotations.json", options)?;
188            zip.write_all(annot.as_bytes())?;
189        }
190
191        if let Some(dict) = &self.dictionary {
192            zip.start_file("dictionary.dict", options)?;
193            zip.write_all(dict.as_bytes())?;
194        }
195
196        let mut rule_names: Vec<_> = self.rules.keys().collect();
197        rule_names.sort();
198
199        for rule_name in rule_names {
200            let file_name = format!("{rule_name}.weir");
201            zip.start_file(file_name, options)?;
202            if let Some(rule) = self.rules.get(rule_name) {
203                zip.write_all(rule.as_bytes())?;
204            }
205        }
206
207        let cursor = zip.finish()?;
208        Ok(cursor.into_inner())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::{Weirpack, WeirpackManifest};
215
216    #[test]
217    fn round_trip_weirpack_bytes() {
218        let mut manifest = WeirpackManifest::new();
219        manifest.set_author("Test Author");
220        manifest.set_version("0.1.0");
221        manifest.set_description("Test pack");
222        manifest.set_license("MIT");
223
224        let mut pack = Weirpack::new(manifest);
225        pack.add_rule("ExampleRule", "expr main test");
226
227        let bytes = pack.to_bytes().expect("serialize weirpack");
228        let parsed = Weirpack::from_bytes(&bytes).expect("deserialize weirpack");
229
230        assert_eq!(parsed.manifest.author().unwrap(), "Test Author");
231        assert_eq!(parsed.manifest.version().unwrap(), "0.1.0");
232        assert_eq!(parsed.manifest.description().unwrap(), "Test pack");
233        assert_eq!(parsed.manifest.license().unwrap(), "MIT");
234        assert_eq!(parsed.rules.get("ExampleRule").unwrap(), "expr main test");
235    }
236}