1mod severity;
2mod spellcheck;
3pub mod toggle;
4
5pub use severity::*;
6pub use spellcheck::*;
7
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::HashMap,
11 fs,
12 path::Path,
13 rc::Rc,
14 sync::{RwLock, RwLockReadGuard},
15};
16
17use crate::serde_any;
18
19lazy_static! {
20 static ref CONFIG_STR: &'static str = include_str!(concat!(
21 env!("CARGO_MANIFEST_DIR"),
22 "/.autocorrectrc.default"
23 ));
24 pub(crate) static ref CURRENT_CONFIG: RwLock<Config> =
25 RwLock::new(Config::from_str(&CONFIG_STR).unwrap());
26}
27
28pub trait ConfigFileTypes {
29 fn get_ext(&self, ext: &str) -> Option<&str>;
30}
31
32impl ConfigFileTypes for HashMap<String, String> {
33 fn get_ext(&self, ext: &str) -> Option<&str> {
34 if let Some(val) = self.get(ext) {
35 return Some(val);
36 }
37
38 if let Some(val) = self.get(&format!("*.{ext}")) {
39 return Some(val);
40 }
41
42 if let Some(val) = self.get(&format!(".{ext}")) {
43 return Some(val);
44 }
45
46 None
47 }
48}
49
50#[derive(Deserialize, Serialize, Default, Clone, Debug)]
51#[serde(rename_all = "camelCase")]
52pub struct Config {
53 #[serde(default)]
54 pub spellcheck: SpellcheckConfig,
55 #[serde(default)]
56 pub rules: HashMap<String, SeverityMode>,
57 #[serde(default)]
59 pub text_rules: HashMap<String, SeverityMode>,
60 #[serde(default)]
62 pub file_types: HashMap<String, String>,
63 #[serde(default)]
64 pub context: HashMap<String, SeverityMode>,
65}
66
67pub fn load_file(config_file: &str) -> Result<Config, Error> {
68 let config_path = Path::new(config_file);
69 if !Path::exists(config_path) {
70 return Ok(Config::default());
71 }
72
73 let config_str = fs::read_to_string(Path::new(config_file))?;
74
75 load(&config_str)
76}
77
78pub fn load(config_str: &str) -> Result<Config, Error> {
79 let config: Config = Config::from_str(config_str)?;
80
81 let new_config: Config = CURRENT_CONFIG.write().unwrap().merge(&config)?;
82
83 Ok(new_config)
84}
85
86#[derive(Debug, Clone)]
87pub struct Error {
88 message: String,
89}
90
91impl std::fmt::Display for Error {
92 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
93 write!(f, "{}", self.message)
94 }
95}
96
97impl From<std::fmt::Error> for Error {
98 fn from(err: std::fmt::Error) -> Error {
99 Error {
100 message: err.to_string(),
101 }
102 }
103}
104
105impl From<serde_any::Error> for Error {
106 fn from(err: serde_any::Error) -> Error {
107 Error {
108 message: format!("{err:?}"),
109 }
110 }
111}
112
113impl From<std::io::Error> for Error {
114 fn from(err: std::io::Error) -> Error {
115 Error {
116 message: err.to_string(),
117 }
118 }
119}
120
121impl From<std::string::String> for Error {
122 fn from(err: std::string::String) -> Error {
123 Error { message: err }
124 }
125}
126
127impl Config {
128 pub fn current() -> Rc<RwLockReadGuard<'static, Config>> {
129 Rc::new(CURRENT_CONFIG.read().unwrap())
130 }
131
132 pub fn get_file_type(&self, ext: &str) -> Option<&str> {
133 self.file_types.get_ext(ext)
134 }
135
136 #[allow(clippy::should_implement_trait)]
137 pub fn from_str(s: &str) -> Result<Self, Error> {
138 let mut config: Config = match serde_any::from_str_any(s) {
139 Ok(config) => config,
140 Err(err) => return Err(format!("Config::from_str parse error: {err:?}").into()),
141 };
142
143 config.prepare();
144
145 Ok(config)
146 }
147
148 pub fn prepare(&mut self) {
149 self.spellcheck.prepare();
150 }
151
152 pub fn merge(&mut self, config: &Config) -> Result<Config, Error> {
153 for (k, v) in config.rules.clone() {
154 self.rules.insert(k, v);
155 }
156
157 if let Some(mode) = config.spellcheck.mode {
159 println!("DEPRECATED: `spellcheck.mode` use `rules.spellcheck` instead since 2.0.0");
160 self.spellcheck.mode = Some(mode);
161 self.rules.insert("spellcheck".to_string(), mode);
162 }
163
164 config.context.iter().for_each(|(k, v)| {
165 self.context.insert(k.to_owned(), v.to_owned());
166 });
167
168 config.text_rules.iter().for_each(|(k, v)| {
169 self.text_rules.insert(k.to_owned(), v.to_owned());
170 });
171
172 config.file_types.iter().for_each(|(k, v)| {
173 self.file_types.insert(k.to_owned(), v.to_owned());
174 });
175
176 self.spellcheck.words = self
177 .spellcheck
178 .words
179 .iter()
180 .chain(config.spellcheck.words.iter())
181 .cloned()
182 .collect();
183
184 self.prepare();
185
186 Ok(self.clone())
187 }
188
189 pub fn is_enabled_context(&self, name: &str) -> bool {
191 if let Some(mode) = self.context.get(name) {
192 return *mode != SeverityMode::Off;
193 }
194
195 false
196 }
197}
198
199static SETUP_ONCE: std::sync::Once = std::sync::Once::new();
201
202#[allow(unused)]
203pub(crate) fn setup_test() {
204 SETUP_ONCE.call_once(|| {
205 let config_str = include_str!(concat!(
206 env!("CARGO_MANIFEST_DIR"),
207 "/tests/.autocorrectrc.test"
208 ));
209 crate::config::load(config_str).unwrap();
210 })
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use indoc::indoc;
217
218 #[test]
219 fn test_parse_json() {
220 let json_str = indoc! {r#"
221 {
222 "rules": {
223 "foo": 1,
224 "bar": "off",
225 "dar": "2"
226 },
227 "textRules": {
228 "hello": 1,
229 "word": 2
230 },
231 "spellcheck": {
232 "mode": 0,
233 "words": [
234 "Foo",
235 "Bar"
236 ]
237 },
238 "fileTypes": {
239 "md": "markdown",
240 "md1": "markdown",
241 "*.ascii": "asciidoc",
242 "Gemfile": "ruby"
243 }
244 }
245 "#};
246 let mut config = Config::from_str(json_str).unwrap();
247
248 assert_eq!(Some(&SeverityMode::Error), config.rules.get("foo"));
249 assert_eq!(Some(&SeverityMode::Off), config.rules.get("bar"));
250 assert_eq!(Some(&SeverityMode::Warning), config.rules.get("dar"));
251
252 assert_eq!(Some(SeverityMode::Off), config.spellcheck.mode);
253 assert_eq!(vec!["Foo", "Bar"], config.spellcheck.words);
254 assert_eq!(Some(&SeverityMode::Error), config.rules.get("foo"));
255 assert_eq!(Some(&SeverityMode::Off), config.rules.get("bar"));
256 assert_eq!(Some(&SeverityMode::Warning), config.rules.get("dar"));
257
258 assert_eq!(Some(&SeverityMode::Error), config.text_rules.get("hello"));
259 assert_eq!(Some(&SeverityMode::Warning), config.text_rules.get("word"));
260
261 assert_eq!(Some(&"ruby".into()), config.file_types.get("Gemfile"));
262 assert_eq!(Some(&"markdown".into()), config.file_types.get("md"));
263 assert_eq!(Some(&"markdown".into()), config.file_types.get("md1"));
264 assert_eq!(Some(&"asciidoc".into()), config.file_types.get("*.ascii"));
265 assert_eq!(None, config.file_types.get("foo"));
266
267 config = Config::from_str(r#"{ "spellcheck": { } }"#).unwrap();
268 assert_eq!(None, config.spellcheck.mode);
269
270 config = Config::from_str(r#"{ "spellcheck": { "mode": 1 } }"#).unwrap();
271 assert_eq!(Some(SeverityMode::Error), config.spellcheck.mode);
272
273 config = Config::from_str(r#"{ "spellcheck": { "mode": 2 } }"#).unwrap();
274 assert_eq!(Some(SeverityMode::Warning), config.spellcheck.mode);
275
276 config = Config::from_str(r#"{ "spellcheck": { "mode": "0" } }"#).unwrap();
277 assert_eq!(Some(SeverityMode::Off), config.spellcheck.mode);
278
279 config = Config::from_str(r#"{ "spellcheck": { "mode": "1" } }"#).unwrap();
280 assert_eq!(Some(SeverityMode::Error), config.spellcheck.mode);
281
282 config = Config::from_str(r#"{ "spellcheck": { "mode": "2" } }"#).unwrap();
283 assert_eq!(Some(SeverityMode::Warning), config.spellcheck.mode);
284
285 config = Config::from_str(r#"{ }"#).unwrap();
286 assert_eq!(None, config.spellcheck.mode);
287
288 config = Config::from_str(r#"{ "spellcheck": { "words" : ["Hello"] } }"#).unwrap();
289 assert_eq!(None, config.spellcheck.mode);
290 assert_eq!(vec!["Hello"], config.spellcheck.words);
291 }
292
293 #[test]
294 fn test_spellcheck_parse_yaml() {
295 let mut config = Config::from_str("spellcheck:\n mode: 0").unwrap();
296 assert_eq!(Some(SeverityMode::Off), config.spellcheck.mode);
297
298 let yaml_str = indoc! {r#"
299 rules:
300 foo: '1'
301 bar: off
302 dar: warning
303 textRules:
304 hello: error
305 word: '0'
306 spellcheck:
307 mode: 1
308 words:
309 - Foo
310 - Bar
311 fileTypes:
312 Foo: foo
313 "#};
314
315 config = Config::from_str(yaml_str).unwrap();
316
317 assert_eq!(Some(&SeverityMode::Error), config.rules.get("foo"));
318 assert_eq!(Some(&SeverityMode::Off), config.rules.get("bar"));
319 assert_eq!(Some(&SeverityMode::Warning), config.rules.get("dar"));
320
321 assert_eq!(Some(&SeverityMode::Error), config.text_rules.get("hello"));
322 assert_eq!(Some(&SeverityMode::Off), config.text_rules.get("word"));
323
324 assert_eq!(Some(SeverityMode::Error), config.spellcheck.mode);
325 assert_eq!(vec!["Foo", "Bar"], config.spellcheck.words);
326
327 assert_eq!(Some(&"foo".to_owned()), config.file_types.get("Foo"));
328
329 config = Config::from_str("").unwrap();
330 assert_eq!(None, config.spellcheck.mode);
331 assert_eq!(Vec::<String>::new(), config.spellcheck.words);
332 }
333
334 #[test]
335 fn test_current_config_with_default_config_file() {
336 let config = Config::current();
337
338 let mut keys: Vec<String> = config.rules.keys().cloned().collect();
339 keys.sort();
340 let mut rule_names: Vec<String> = crate::rule::default_rule_names();
341 rule_names.sort();
342 assert_eq!(rule_names, keys);
343
344 for (k, v) in config.rules.clone() {
345 match k.as_str() {
346 "spellcheck" => assert_eq!(SeverityMode::Warning, v),
347 "space-dash" => assert_eq!(SeverityMode::Error, v),
348 "space-dollar" => assert_eq!(SeverityMode::Off, v),
349 _ => assert_eq!(SeverityMode::Error, v),
350 }
351 }
352
353 assert_eq!(None, config.spellcheck.mode);
354 assert!(!config.spellcheck.words.is_empty());
355 assert!(!config.spellcheck.word_map.is_empty());
356 }
357
358 #[test]
359 fn test_merge_config() {
360 let mut config = Config {
361 rules: map! {
362 "foo".to_owned() => SeverityMode::Error,
363 },
364 context: map! {
365 "foo".to_owned() => SeverityMode::Error,
366 "foo1".to_owned() => SeverityMode::Off,
367 },
368 text_rules: map! {
369 "a".to_owned() => SeverityMode::Off,
370 "hello".to_owned() => SeverityMode::Error
371 },
372 file_types: map! {
373 "a".to_owned() => "A".to_owned(),
374 "foo".to_owned() => "Foo".to_owned()
375 },
376 spellcheck: SpellcheckConfig {
377 mode: Some(SeverityMode::Warning),
378 words: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()],
379 ..Default::default()
380 },
381 };
382
383 let config1 = Config {
384 rules: map! {
385 "bar".to_owned() => SeverityMode::Warning,
386 },
387 context: map! {
388 "foo".to_owned() => SeverityMode::Warning,
389 "foo2".to_owned() => SeverityMode::Off,
390 },
391 text_rules: map! {
392 "world".to_owned() => SeverityMode::Off
393 },
394 file_types: map! {
395 "foo".to_owned() => "Foo New".to_owned(),
396 "bar".to_owned() => "Bar".to_owned()
397 },
398 spellcheck: SpellcheckConfig {
399 mode: Some(SeverityMode::Off),
400 words: vec!["foo1".to_string(), "bar1".to_string()],
401 ..Default::default()
402 },
403 };
404
405 config.merge(&config1).unwrap();
406
407 let new_rules = map! {
408 "spellcheck".to_owned() => SeverityMode::Off,
409 "foo".to_owned() => SeverityMode::Error,
410 "bar".to_owned() => SeverityMode::Warning
411 };
412 assert_eq!(new_rules, config.rules);
413
414 assert_eq!(config.context.get("foo"), Some(&SeverityMode::Warning));
415 assert_eq!(config.context.get("foo1"), Some(&SeverityMode::Off));
416 assert_eq!(config.context.get("foo2"), Some(&SeverityMode::Off));
417
418 let new_text_rules = map! {
419 "a".to_owned() => SeverityMode::Off,
420 "hello".to_owned() => SeverityMode::Error,
421 "world".to_owned() => SeverityMode::Off
422 };
423 assert_eq!(new_text_rules, config.text_rules);
424
425 let new_file_types = map! {
426 "a".to_owned() => "A".to_owned(),
427 "foo".to_owned() => "Foo New".to_owned(),
428 "bar".to_owned() => "Bar".to_owned(),
429 };
430 assert_eq!(new_file_types, config.file_types);
431
432 assert_eq!(config.spellcheck.mode, Some(SeverityMode::Off));
433 assert_eq!(
434 config.spellcheck.words,
435 vec![
436 "foo".to_string(),
437 "bar".to_string(),
438 "baz".to_string(),
439 "foo1".to_string(),
440 "bar1".to_string()
441 ]
442 );
443 }
444
445 #[test]
446 fn test_file_types_get_ext() {
447 let config = Config {
448 file_types: map! {
449 "*.rs".to_owned() => "rust".to_owned(),
450 ".asc".to_owned() => "asciidoc".to_owned(),
451 "rb".to_owned() => "ruby".to_owned(),
452 "Gemfile".to_owned() => "ruby".to_owned(),
453 },
454 ..Default::default()
455 };
456
457 assert_eq!(Some("rust"), config.file_types.get_ext("rs"));
458 assert_eq!(Some("asciidoc"), config.file_types.get_ext("asc"));
459 assert_eq!(Some("ruby"), config.file_types.get_ext("rb"));
460 assert_eq!(Some("ruby"), config.file_types.get_ext("Gemfile"));
461 assert_eq!(None, config.file_types.get_ext("foo"));
462 }
463}