autocorrect/config/
mod.rs

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    // Speical text to ignore
58    #[serde(default)]
59    pub text_rules: HashMap<String, SeverityMode>,
60    // Addition file types map, high priority than default
61    #[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        // DEPRECATED: since 2.0.0, remove this in 3.0.0
158        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    /// Check is enable format in context
190    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
199// Setup config for test for load tests/.autocorrectrc.test
200static 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}