harper_core/weirpack/
mod.rs1use 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#[derive(Debug, Clone, Default)]
23pub struct Weirpack {
24 pub rules: HashMap<String, String>,
25 pub dictionary: Option<String>,
27 pub annotations: Option<String>,
29 pub manifest: WeirpackManifest,
30}
31
32impl Weirpack {
33 pub fn new(manifest: WeirpackManifest) -> Self {
35 Self {
36 rules: HashMap::new(),
37 annotations: None,
38 dictionary: None,
39 manifest,
40 }
41 }
42
43 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 pub fn remove_rule(&mut self, name: &str) -> Option<String> {
50 self.rules.remove(name)
51 }
52
53 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 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 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 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 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 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 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}