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 group.add_chunk_expr_linter(name, linter);
76 group.config.set_rule_enabled(name, true);
77 }
78
79 Ok(group)
80 }
81
82 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 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 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 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 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}