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            match linter.into_sentence_linter() {
76                Ok(linter) => group.add_sentence_expr_linter(name, linter),
77                Err(linter) => group.add_chunk_expr_linter(
78                    name,
79                    linter
80                        .into_chunk_linter()
81                        .unwrap_or_else(|_| unreachable!()),
82                ),
83            };
84            group.config.set_rule_enabled(name, true);
85        }
86
87        Ok(group)
88    }
89
90    /// Load a Weirpack from bytes.
91    pub fn from_reader(mut reader: impl Read) -> Result<Self, Error> {
92        let mut bytes = Vec::new();
93        reader.read_to_end(&mut bytes)?;
94        Self::from_bytes(&bytes)
95    }
96
97    /// Write the Weirpack to bytes.
98    pub fn write_to(&self, mut writer: impl Write) -> Result<(), Error> {
99        let bytes = self.to_bytes()?;
100        writer.write_all(&bytes)?;
101        Ok(())
102    }
103
104    /// Loads the dictionary that may or may not be contained within the Weirpack.
105    ///
106    /// The dictionary is in the Rune format and thus is composed of two files, `annotations.json`
107    /// and `dictionary.dict`.
108    ///
109    /// Returns `None` if the relevant files are not present in the Weirpack.
110    pub fn load_dictionary(&self) -> Result<Option<MutableDictionary>, Error> {
111        if let Some(dict) = &self.dictionary
112            && let Some(annot) = &self.annotations
113        {
114            Ok(Some(
115                MutableDictionary::from_rune_files(dict, annot)
116                    .map_err(|_| Error::InvalidDictionaryFormat)?,
117            ))
118        } else {
119            Ok(None)
120        }
121    }
122
123    /// Load a Weirpack from bytes.
124    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
125        let cursor = std::io::Cursor::new(bytes);
126        let mut archive = ZipArchive::new(cursor)?;
127
128        let mut manifest = None;
129        let mut rules = HashMap::new();
130        let mut dictionary = None;
131        let mut annotations = None;
132
133        for i in 0..archive.len() {
134            let mut file = archive.by_index(i)?;
135            if file.is_dir() {
136                continue;
137            }
138
139            let name = file.name().to_string();
140            if name == "manifest.json" {
141                if manifest.is_some() {
142                    return Err(Error::DuplicateManifest("manifest.json"));
143                }
144                let manifest_data = WeirpackManifest::from_reader(&mut file)?;
145                manifest = Some(manifest_data);
146                continue;
147            }
148
149            if name.ends_with(".weir") {
150                let path = Path::new(&name);
151                let file_name = path
152                    .file_name()
153                    .and_then(|segment| segment.to_str())
154                    .ok_or_else(|| Error::InvalidRuleFileName(name.clone()))?;
155                let rule_name = Path::new(file_name)
156                    .file_stem()
157                    .and_then(|stem| stem.to_str())
158                    .ok_or_else(|| Error::InvalidRuleFileName(name.clone()))?;
159
160                let mut contents = String::new();
161                file.read_to_string(&mut contents)?;
162                rules.insert(rule_name.to_string(), contents);
163            } else if name == "dictionary.dict" {
164                let mut contents = String::new();
165                file.read_to_string(&mut contents)?;
166                dictionary = Some(contents);
167            } else if name == "annotations.json" {
168                let mut contents = String::new();
169                file.read_to_string(&mut contents)?;
170                annotations = Some(contents);
171            }
172        }
173
174        let manifest = manifest.ok_or(Error::MissingManifest("manifest.json"))?;
175
176        Ok(Self {
177            rules,
178            manifest,
179            annotations,
180            dictionary,
181        })
182    }
183
184    /// Write a Weirpack into bytes.
185    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
186        let mut zip = ZipWriter::new(std::io::Cursor::new(Vec::new()));
187        let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated);
188
189        let mut manifest_bytes = Vec::new();
190        self.manifest.write_to(&mut manifest_bytes)?;
191        zip.start_file("manifest.json", options)?;
192        zip.write_all(&manifest_bytes)?;
193
194        if let Some(annot) = &self.annotations {
195            zip.start_file("annotations.json", options)?;
196            zip.write_all(annot.as_bytes())?;
197        }
198
199        if let Some(dict) = &self.dictionary {
200            zip.start_file("dictionary.dict", options)?;
201            zip.write_all(dict.as_bytes())?;
202        }
203
204        let mut rule_names: Vec<_> = self.rules.keys().collect();
205        rule_names.sort();
206
207        for rule_name in rule_names {
208            let file_name = format!("{rule_name}.weir");
209            zip.start_file(file_name, options)?;
210            if let Some(rule) = self.rules.get(rule_name) {
211                zip.write_all(rule.as_bytes())?;
212            }
213        }
214
215        let cursor = zip.finish()?;
216        Ok(cursor.into_inner())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::{Weirpack, WeirpackManifest};
223
224    #[test]
225    fn round_trip_weirpack_bytes() {
226        let mut manifest = WeirpackManifest::new();
227        manifest.set_author("Test Author");
228        manifest.set_version("0.1.0");
229        manifest.set_description("Test pack");
230        manifest.set_license("MIT");
231
232        let mut pack = Weirpack::new(manifest);
233        pack.add_rule("ExampleRule", "expr main test");
234
235        let bytes = pack.to_bytes().expect("serialize weirpack");
236        let parsed = Weirpack::from_bytes(&bytes).expect("deserialize weirpack");
237
238        assert_eq!(parsed.manifest.author().unwrap(), "Test Author");
239        assert_eq!(parsed.manifest.version().unwrap(), "0.1.0");
240        assert_eq!(parsed.manifest.description().unwrap(), "Test pack");
241        assert_eq!(parsed.manifest.license().unwrap(), "MIT");
242        assert_eq!(parsed.rules.get("ExampleRule").unwrap(), "expr main test");
243    }
244}