cargo_spellcheck/config/
mod.rs1#![allow(missing_docs)]
10
11pub mod args;
12
13mod regex;
14pub use self::regex::*;
15
16mod reflow;
17pub use self::reflow::*;
18
19mod hunspell;
20pub use self::hunspell::*;
21
22mod nlprules;
23pub use self::nlprules::*;
24
25mod search_dirs;
26pub use search_dirs::*;
27
28mod iso;
29pub use iso::*;
30
31use crate::errors::*;
32use crate::Detector;
33use fancy_regex::Regex;
34
35use fs_err as fs;
36use serde::{Deserialize, Serialize};
37use std::convert::AsRef;
38use std::fmt;
39use std::io::Read;
40use std::path::{Path, PathBuf};
41
42#[derive(Deserialize, Serialize, Debug, Clone)]
43#[serde(deny_unknown_fields)]
44pub struct Config {
45 #[serde(default)]
47 #[serde(alias = "dev-comments")]
48 #[serde(alias = "devcomments")]
49 pub dev_comments: bool,
50
51 #[serde(default)]
52 #[serde(alias = "skip-readme")]
53 #[serde(alias = "skipreadme")]
54 pub skip_readme: bool,
55
56 #[serde(alias = "Hunspell")]
57 #[serde(default = "default_hunspell")]
58 pub hunspell: Option<HunspellConfig>,
59
60 #[serde(alias = "ZSpell")]
61 #[serde(default = "default_zspell")]
62 pub zet: Option<ZetConfig>,
63
64 #[serde(alias = "Spellbook")]
65 #[serde(alias = "book")]
66 #[serde(default = "default_spellbook")]
67 pub spellbook: Option<SpellbookConfig>,
68
69 #[serde(alias = "Nlp")]
70 #[serde(alias = "NLP")]
71 #[serde(alias = "nlp")]
72 #[serde(alias = "NLP")]
73 #[serde(alias = "NlpRules")]
74 #[serde(default = "default_nlprules")]
75 pub nlprules: Option<NlpRulesConfig>,
76
77 #[serde(alias = "ReFlow")]
78 #[serde(alias = "Reflow")]
79 pub reflow: Option<ReflowConfig>,
80}
81
82impl Config {
83 const QUALIFIER: &'static str = "rs";
84 const ORGANIZATION: &'static str = "fff";
85 const APPLICATION: &'static str = "cargo_spellcheck";
86
87 fn sanitize_paths(&mut self, base: &Path) -> Result<()> {
89 if let Some(ref mut hunspell) = self.hunspell {
90 hunspell.sanitize_paths(base)?;
91 }
92 if let Some(ref mut zspell) = self.zet {
93 zspell.sanitize_paths(base)?;
94 }
95 if let Some(ref mut spellbook) = self.spellbook {
96 spellbook.sanitize_paths(base)?;
97 }
98 Ok(())
99 }
100
101 pub fn parse<S: AsRef<str>>(s: S) -> Result<Self> {
102 Ok(toml::from_str(s.as_ref())?)
103 }
104
105 pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
106 let (contents, path) = match Self::load_content(path) {
107 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
108 return Ok(None);
109 }
110 Err(e) => bail!(e),
111 Ok(contents) => contents,
112 };
113 Self::parse(&contents)
114 .wrap_err_with(|| {
115 eyre!(
116 "Syntax of a given config file({}) is broken",
117 path.display()
118 )
119 })
120 .and_then(|mut cfg| {
121 if let Some(base) = path.parent() {
122 cfg.sanitize_paths(base)?;
123 }
124 Ok(Some(cfg))
125 })
126 }
127
128 pub fn load_content<P: AsRef<Path>>(path: P) -> std::io::Result<(String, PathBuf)> {
129 let path = path.as_ref().canonicalize()?;
130 let mut file = fs::File::open(&path)?;
131
132 let mut contents = String::with_capacity(1024);
133 file.read_to_string(&mut contents)?;
134 Ok((contents, path))
135 }
136
137 pub fn load() -> Result<Option<Self>> {
138 if let Some(base) = directories::BaseDirs::new() {
139 Self::load_from(
140 base.config_dir()
141 .join("cargo_spellcheck")
142 .join("config.toml"),
143 )
144 } else {
145 bail!("No idea where your config directory is located. XDG compliance would be nice.")
146 }
147 }
148
149 pub fn to_toml(&self) -> Result<String> {
150 toml::to_string(self).wrap_err_with(|| eyre!("Failed to convert to toml"))
151 }
152
153 pub fn write_values_to<W: std::io::Write>(&self, mut writer: W) -> Result<Self> {
154 let s = self.to_toml()?;
155 writer.write_all(s.as_bytes())?;
156 Ok(self.clone())
157 }
158
159 pub fn write_values_to_path<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
160 let path = path.as_ref();
161
162 if let Some(path) = path.parent() {
163 fs::create_dir_all(path).wrap_err_with(|| {
164 eyre!("Failed to create config parent dirs {}", path.display())
165 })?;
166 }
167
168 let file = fs::OpenOptions::new()
169 .create(true)
170 .write(true)
171 .truncate(true)
172 .open(path)
173 .wrap_err_with(|| eyre!("Failed to write default values to {}", path.display()))?;
174
175 let writer = std::io::BufWriter::new(file);
176
177 self.write_values_to(writer)
178 .wrap_err_with(|| eyre!("Failed to write default config to {}", path.display()))
179 }
180
181 pub fn write_values_to_default_path(&self) -> Result<Self> {
182 let path = Self::default_path()?;
183 self.write_values_to_path(path)
184 }
185
186 pub fn write_default_values_to<P: AsRef<Path>>(path: P) -> Result<Self> {
187 Self::default().write_values_to_path(path)
188 }
189
190 pub fn default_path() -> Result<PathBuf> {
191 if let Some(base) =
192 directories::ProjectDirs::from(Self::QUALIFIER, Self::ORGANIZATION, Self::APPLICATION)
193 {
194 Ok(base.config_dir().join("config.toml"))
195 } else {
196 bail!("No idea where your config directory is located. `$HOME` must be set.")
197 }
198 }
199
200 pub fn project_config(manifest_dir: impl AsRef<Path>) -> Result<PathBuf> {
202 let path = manifest_dir
203 .as_ref()
204 .to_owned()
205 .join(".config")
206 .join("spellcheck.toml");
207
208 let path = path.canonicalize()?;
209
210 if path.is_file() {
211 Ok(path)
212 } else {
213 bail!(
214 "Local project dir config {} does not exist or is not a file.",
215 path.display()
216 )
217 }
218 }
219
220 pub fn write_default_values() -> Result<Self> {
221 let d = Self::default_path()?;
222 Self::write_default_values_to(d.join("config.toml"))
223 }
224
225 pub fn is_enabled(&self, detector: Detector) -> bool {
226 match detector {
227 Detector::Hunspell => self.hunspell.is_some(),
228 Detector::ZSpell => self.zet.is_some(),
229 Detector::Spellbook => self.spellbook.is_some(),
230 Detector::NlpRules => self.nlprules.is_some(),
231 Detector::Reflow => self.reflow.is_some(),
232 #[cfg(test)]
233 Detector::Dummy => true,
234 }
235 }
236
237 pub fn full() -> Self {
238 Default::default()
239 }
240}
241
242fn default_nlprules() -> Option<NlpRulesConfig> {
243 if cfg!(feature = "nlprules") {
244 Some(NlpRulesConfig::default())
245 } else {
246 log::warn!("Cannot enable nlprules, since it wasn't compiled with `nlprules` as checker");
247 None
248 }
249}
250
251fn default_hunspell() -> Option<HunspellConfig> {
252 Some(HunspellConfig::default())
253}
254fn default_zspell() -> Option<ZetConfig> {
255 Some(ZetConfig::default())
256}
257fn default_spellbook() -> Option<SpellbookConfig> {
258 Some(SpellbookConfig::default())
259}
260
261impl Default for Config {
262 fn default() -> Self {
263 Self {
264 dev_comments: false,
265 skip_readme: false,
266 hunspell: default_hunspell(),
267 zet: default_zspell(),
268 spellbook: default_spellbook(),
269 nlprules: default_nlprules(),
270 reflow: Some(ReflowConfig::default()),
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use assert_matches::assert_matches;
279
280 #[test]
281 fn can_serialize_to_toml() {
282 let config = dbg!(Config::full());
283 assert_matches!(config.to_toml(), Ok(_s));
284 }
285
286 #[test]
287 fn project_config_works() {
288 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
289 .join(".config")
290 .join("spellcheck.toml");
291 assert_matches!(Config::load_from(&path), Ok(_));
292 }
293
294 #[test]
295 fn all() {
296 let _ = Config::parse(
297 r#"
298dev_comments = true
299skip-readme = true
300
301[Hunspell]
302lang = "en_US"
303search_dirs = ["/usr/lib64/hunspell"]
304extra_dictionaries = ["/home/bernhard/test.dic"]
305 "#,
306 )
307 .unwrap();
308 }
309
310 #[test]
311 fn empty() {
312 assert!(Config::parse(
313 r#"
314 "#,
315 )
316 .is_ok());
317 }
318 #[test]
319 fn partial_1() {
320 let _cfg = Config::parse(
321 r#"
322[hunspell]
323lang = "en_GB"
324search_dirs = ["/usr/lib64/hunspell"]
325extra_dictionaries = ["/home/bernhard/test.dic"]
326 "#,
327 )
328 .unwrap();
329 }
330
331 #[test]
332 fn partial_3() {
333 let cfg = Config::parse(
334 r#"
335[Hunspell]
336lang = "de_AT"
337search_dirs = ["/usr/lib64/hunspell"]
338extra_dictionaries = ["/home/bernhard/test.dic"]
339 "#,
340 )
341 .unwrap();
342 let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
343 }
344
345 #[test]
346 fn partial_4() {
347 let cfg = Config::parse(
348 r#"
349[Hunspell]
350lang = "en_US"
351 "#,
352 )
353 .unwrap();
354 let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
355 }
356
357 #[test]
358 fn partial_5() {
359 assert!(Config::parse(
360 r#"
361[hUNspell]
362lang = "en_US"
363 "#,
364 )
365 .is_err());
366 }
367
368 #[test]
369 fn partial_6() {
370 let cfg = Config::parse(
371 r#"
372[hunspell]
373 "#,
374 )
375 .unwrap();
376 let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
377 }
378
379 #[test]
380 fn partial_7() {
381 let cfg = Config::parse(
382 r#"
383[Hunspell.quirks]
384allow_concatenation = true
385allow_dashes = true
386transform_regex = ["^'([^\\s])'$", "^[0-9]+x$"]
387 "#,
388 )
389 .unwrap();
390 let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
391 }
392
393 #[test]
394 fn partial_8() {
395 let cfg = Config::parse(
396 r#"
397[Hunspell]
398search_dirs = ["/search/1", "/search/2"]
399skip_os_lookups = true
400 "#,
401 )
402 .unwrap();
403
404 let hunspell: HunspellConfig = cfg.hunspell.expect("Must contain hunspell cfg");
405 assert!(hunspell.skip_os_lookups);
406
407 let search_dirs = hunspell.search_dirs;
408 let search_dirs2: Vec<_> = search_dirs.as_ref().clone();
409 assert!(!search_dirs2.is_empty());
410
411 assert_eq!(search_dirs.iter(false).count(), 2);
412
413 #[cfg(target_os = "linux")]
414 assert_eq!(search_dirs.iter(true).count(), 5);
415
416 #[cfg(target_os = "windows")]
417 assert_eq!(search_dirs.iter(true).count(), 2);
418
419 #[cfg(target_os = "macos")]
420 assert!(search_dirs.iter(true).count() >= 3);
421 }
422
423 #[test]
424 fn partial_9() {
425 let cfg = Config::parse(
426 r#"
427[Reflow]
428max_line_length = 42
429"#,
430 )
431 .unwrap();
432 assert_eq!(
433 cfg.reflow.expect("Must contain reflow cfg").max_line_length,
434 42
435 );
436 }
437}