1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct Config {
8 #[serde(default)]
9 pub engines: EngineConfig,
10 #[serde(default)]
11 pub rules: HashMap<String, RuleConfig>,
12 #[serde(default = "default_exclude")]
13 pub exclude: Vec<String>,
14 #[serde(default)]
15 pub auto_fix: Vec<AutoFixRule>,
16 #[serde(default)]
17 pub performance: PerformanceConfig,
18 #[serde(default)]
19 pub dictionaries: DictionaryConfig,
20 #[serde(default)]
21 pub languages: LanguageConfig,
22 #[serde(default)]
23 pub workspace: WorkspaceConfig,
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone, Default)]
39pub struct LanguageConfig {
40 #[serde(default)]
42 pub extensions: HashMap<String, Vec<String>>,
43 #[serde(default)]
45 pub latex: LaTeXConfig,
46}
47
48#[derive(Debug, Serialize, Deserialize, Clone, Default)]
58pub struct LaTeXConfig {
59 #[serde(default)]
62 pub skip_environments: Vec<String>,
63 #[serde(default)]
67 pub skip_commands: Vec<String>,
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone, Default)]
77pub struct WorkspaceConfig {
78 #[serde(default)]
81 pub index_on_open: bool,
82 #[serde(default)]
85 pub db_path: Option<String>,
86}
87
88#[derive(Debug, Serialize, Deserialize, Clone)]
91pub struct PerformanceConfig {
92 #[serde(default)]
94 pub high_performance_mode: bool,
95 #[serde(default = "default_debounce_ms")]
97 pub debounce_ms: u64,
98 #[serde(default)]
100 pub max_file_size: usize,
101}
102
103impl Default for PerformanceConfig {
104 fn default() -> Self {
105 Self {
106 high_performance_mode: false,
107 debounce_ms: 300,
108 max_file_size: 0,
109 }
110 }
111}
112
113const fn default_debounce_ms() -> u64 {
114 300
115}
116
117#[derive(Debug, Serialize, Deserialize, Clone)]
119pub struct DictionaryConfig {
120 #[serde(default = "default_true")]
123 pub bundled: bool,
124 #[serde(default)]
127 pub paths: Vec<String>,
128}
129
130impl Default for DictionaryConfig {
131 fn default() -> Self {
132 Self {
133 bundled: true,
134 paths: Vec::new(),
135 }
136 }
137}
138
139#[derive(Debug, Serialize, Deserialize, Clone)]
141pub struct AutoFixRule {
142 pub find: String,
144 pub replace: String,
146 #[serde(default)]
148 pub context: Option<String>,
149 #[serde(default)]
151 pub description: Option<String>,
152}
153
154#[derive(Debug, Serialize, Deserialize, Clone)]
155pub struct EngineConfig {
156 #[serde(
157 default = "default_harper_config",
158 deserialize_with = "deser_engine_or_bool"
159 )]
160 pub harper: HarperConfig,
161 #[serde(default, deserialize_with = "deser_engine_or_bool")]
162 pub languagetool: LanguageToolConfig,
163 #[serde(default, deserialize_with = "deser_engine_or_bool")]
164 pub vale: ValeConfig,
165 #[serde(default, deserialize_with = "deser_engine_or_bool")]
166 pub proselint: ProselintConfig,
167 #[serde(default)]
169 pub external: Vec<ExternalProvider>,
170 #[serde(default)]
172 pub wasm_plugins: Vec<WasmPlugin>,
173 #[serde(default = "default_spell_language")]
175 pub spell_language: String,
176}
177
178fn deser_engine_or_bool<'de, D, T>(deserializer: D) -> Result<T, D::Error>
181where
182 D: serde::Deserializer<'de>,
183 T: Deserialize<'de> + EngineToggle + Default,
184{
185 #[derive(Deserialize)]
186 #[serde(untagged)]
187 enum BoolOrStruct<T> {
188 Bool(bool),
189 Struct(T),
190 }
191
192 match BoolOrStruct::deserialize(deserializer)? {
193 BoolOrStruct::Bool(b) => {
194 let mut cfg = T::default();
195 cfg.set_enabled(b);
196 Ok(cfg)
197 }
198 BoolOrStruct::Struct(s) => Ok(s),
199 }
200}
201
202pub trait EngineToggle {
204 fn enabled(&self) -> bool;
205 fn set_enabled(&mut self, v: bool);
206}
207
208#[derive(Debug, Serialize, Deserialize, Clone)]
210pub struct HarperConfig {
211 #[serde(default = "default_true")]
212 pub enabled: bool,
213 #[serde(default = "default_dialect")]
215 pub dialect: String,
216 #[serde(default)]
219 pub linters: HashMap<String, bool>,
220}
221
222impl Default for HarperConfig {
223 fn default() -> Self {
224 Self {
225 enabled: true,
226 dialect: "American".to_string(),
227 linters: HashMap::new(),
228 }
229 }
230}
231
232fn default_harper_config() -> HarperConfig {
233 HarperConfig::default()
234}
235
236fn default_dialect() -> String {
237 "American".to_string()
238}
239
240impl EngineToggle for HarperConfig {
241 fn enabled(&self) -> bool {
242 self.enabled
243 }
244 fn set_enabled(&mut self, v: bool) {
245 self.enabled = v;
246 }
247}
248
249#[derive(Debug, Serialize, Deserialize, Clone)]
251pub struct LanguageToolConfig {
252 #[serde(default)]
253 pub enabled: bool,
254 #[serde(default = "default_lt_url")]
256 pub url: String,
257 #[serde(default = "default_lt_level")]
259 pub level: String,
260 #[serde(default)]
262 pub mother_tongue: Option<String>,
263 #[serde(default)]
265 pub disabled_rules: Vec<String>,
266 #[serde(default)]
268 pub enabled_rules: Vec<String>,
269 #[serde(default)]
271 pub disabled_categories: Vec<String>,
272 #[serde(default)]
274 pub enabled_categories: Vec<String>,
275}
276
277impl Default for LanguageToolConfig {
278 fn default() -> Self {
279 Self {
280 enabled: false,
281 url: default_lt_url(),
282 level: "default".to_string(),
283 mother_tongue: None,
284 disabled_rules: Vec::new(),
285 enabled_rules: Vec::new(),
286 disabled_categories: Vec::new(),
287 enabled_categories: Vec::new(),
288 }
289 }
290}
291
292fn default_lt_level() -> String {
293 "default".to_string()
294}
295
296impl EngineToggle for LanguageToolConfig {
297 fn enabled(&self) -> bool {
298 self.enabled
299 }
300 fn set_enabled(&mut self, v: bool) {
301 self.enabled = v;
302 }
303}
304
305#[derive(Debug, Default, Serialize, Deserialize, Clone)]
307pub struct ValeConfig {
308 #[serde(default)]
309 pub enabled: bool,
310 #[serde(default)]
312 pub config: Option<String>,
313}
314
315impl EngineToggle for ValeConfig {
316 fn enabled(&self) -> bool {
317 self.enabled
318 }
319 fn set_enabled(&mut self, v: bool) {
320 self.enabled = v;
321 }
322}
323
324#[derive(Debug, Default, Serialize, Deserialize, Clone)]
326pub struct ProselintConfig {
327 #[serde(default)]
328 pub enabled: bool,
329 #[serde(default)]
331 pub config: Option<String>,
332}
333
334impl EngineToggle for ProselintConfig {
335 fn enabled(&self) -> bool {
336 self.enabled
337 }
338 fn set_enabled(&mut self, v: bool) {
339 self.enabled = v;
340 }
341}
342
343#[derive(Debug, Serialize, Deserialize, Clone)]
348pub struct ExternalProvider {
349 pub name: String,
351 pub command: String,
353 #[serde(default)]
355 pub args: Vec<String>,
356 #[serde(default)]
358 pub extensions: Vec<String>,
359}
360
361#[derive(Debug, Serialize, Deserialize, Clone)]
366pub struct WasmPlugin {
367 pub name: String,
369 pub path: String,
371 #[serde(default)]
373 pub extensions: Vec<String>,
374}
375
376impl Default for EngineConfig {
377 fn default() -> Self {
378 Self {
379 harper: HarperConfig::default(),
380 languagetool: LanguageToolConfig::default(),
381 vale: ValeConfig::default(),
382 proselint: ProselintConfig::default(),
383 external: Vec::new(),
384 wasm_plugins: Vec::new(),
385 spell_language: default_spell_language(),
386 }
387 }
388}
389
390#[derive(Debug, Serialize, Deserialize, Clone)]
391pub struct RuleConfig {
392 pub severity: Option<String>, }
394
395const fn default_true() -> bool {
396 true
397}
398fn default_lt_url() -> String {
399 "http://localhost:8010".to_string()
400}
401fn default_spell_language() -> String {
402 "en-US".to_string()
403}
404fn default_exclude() -> Vec<String> {
405 vec![
406 "node_modules/**".to_string(),
407 ".git/**".to_string(),
408 "target/**".to_string(),
409 "dist/**".to_string(),
410 "build/**".to_string(),
411 ".next/**".to_string(),
412 ".nuxt/**".to_string(),
413 "vendor/**".to_string(),
414 "__pycache__/**".to_string(),
415 ".venv/**".to_string(),
416 "venv/**".to_string(),
417 ".tox/**".to_string(),
418 ".mypy_cache/**".to_string(),
419 "*.min.js".to_string(),
420 "*.min.css".to_string(),
421 "*.bundle.js".to_string(),
422 "package-lock.json".to_string(),
423 "yarn.lock".to_string(),
424 "pnpm-lock.yaml".to_string(),
425 ]
426}
427
428impl Config {
429 pub fn load(workspace_root: &Path) -> Result<Self> {
430 let yaml_path = workspace_root.join(".languagecheck.yaml");
432 let yml_path = workspace_root.join(".languagecheck.yml");
433 let json_path = workspace_root.join(".languagecheck.json");
434
435 if yaml_path.exists() {
436 let content = std::fs::read_to_string(yaml_path)?;
437 let config: Self = serde_yaml::from_str(&content)?;
438 Ok(config)
439 } else if yml_path.exists() {
440 let content = std::fs::read_to_string(yml_path)?;
441 let config: Self = serde_yaml::from_str(&content)?;
442 Ok(config)
443 } else if json_path.exists() {
444 let content = std::fs::read_to_string(json_path)?;
445 let config: Self = serde_json::from_str(&content)?;
446 Ok(config)
447 } else {
448 Ok(Self::default())
449 }
450 }
451
452 #[must_use]
455 pub fn apply_auto_fixes(&self, text: &str) -> (String, usize) {
456 let mut result = text.to_string();
457 let mut total = 0;
458
459 for rule in &self.auto_fix {
460 if let Some(ctx) = &rule.context
461 && !result.contains(ctx.as_str())
462 {
463 continue;
464 }
465 let count = result.matches(&rule.find).count();
466 if count > 0 {
467 result = result.replace(&rule.find, &rule.replace);
468 total += count;
469 }
470 }
471
472 (result, total)
473 }
474}
475
476impl Default for Config {
477 fn default() -> Self {
478 Self {
479 engines: EngineConfig::default(),
480 rules: HashMap::new(),
481 exclude: default_exclude(),
482 auto_fix: Vec::new(),
483 performance: PerformanceConfig::default(),
484 dictionaries: DictionaryConfig::default(),
485 languages: LanguageConfig::default(),
486 workspace: WorkspaceConfig::default(),
487 }
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn default_config_has_harper_enabled_lt_disabled() {
497 let config = Config::default();
498 assert!(config.engines.harper.enabled);
499 assert!(!config.engines.languagetool.enabled);
500 }
501
502 #[test]
503 fn default_config_has_standard_excludes() {
504 let config = Config::default();
505 assert!(config.exclude.contains(&"node_modules/**".to_string()));
506 assert!(config.exclude.contains(&".git/**".to_string()));
507 assert!(config.exclude.contains(&"target/**".to_string()));
508 assert!(config.exclude.contains(&"dist/**".to_string()));
509 assert!(config.exclude.contains(&"vendor/**".to_string()));
510 }
511
512 #[test]
513 fn default_lt_url() {
514 let config = Config::default();
515 assert_eq!(config.engines.languagetool.url, "http://localhost:8010");
516 }
517
518 #[test]
519 fn load_from_json_string() {
520 let json = r#"{
521 "engines": { "harper": true, "languagetool": false },
522 "rules": { "spelling.typo": { "severity": "warning" } }
523 }"#;
524 let config: Config = serde_json::from_str(json).unwrap();
525 assert!(config.engines.harper.enabled);
526 assert!(!config.engines.languagetool.enabled);
527 assert!(config.rules.contains_key("spelling.typo"));
528 assert_eq!(
529 config.rules["spelling.typo"].severity.as_deref(),
530 Some("warning")
531 );
532 }
533
534 #[test]
535 fn load_partial_json_uses_defaults() {
536 let json = r#"{}"#;
537 let config: Config = serde_json::from_str(json).unwrap();
538 assert!(config.engines.harper.enabled);
539 assert!(!config.engines.languagetool.enabled);
540 assert!(config.rules.is_empty());
541 }
542
543 #[test]
544 fn load_from_json_file() {
545 let dir = std::env::temp_dir().join("lang_check_test_config_json");
546 let _ = std::fs::remove_dir_all(&dir);
547 std::fs::create_dir_all(&dir).unwrap();
548
549 let config_path = dir.join(".languagecheck.json");
550 std::fs::write(
551 &config_path,
552 r#"{"engines": {"harper": false, "languagetool": true}}"#,
553 )
554 .unwrap();
555
556 let config = Config::load(&dir).unwrap();
557 assert!(!config.engines.harper.enabled);
558 assert!(config.engines.languagetool.enabled);
559
560 let _ = std::fs::remove_dir_all(&dir);
561 }
562
563 #[test]
564 fn load_from_yaml_file() {
565 let dir = std::env::temp_dir().join("lang_check_test_config_yaml");
566 let _ = std::fs::remove_dir_all(&dir);
567 std::fs::create_dir_all(&dir).unwrap();
568
569 let config_path = dir.join(".languagecheck.yaml");
570 std::fs::write(
571 &config_path,
572 "engines:\n harper: false\n languagetool: true\n",
573 )
574 .unwrap();
575
576 let config = Config::load(&dir).unwrap();
577 assert!(!config.engines.harper.enabled);
578 assert!(config.engines.languagetool.enabled);
579
580 let _ = std::fs::remove_dir_all(&dir);
581 }
582
583 #[test]
584 fn yaml_takes_precedence_over_json() {
585 let dir = std::env::temp_dir().join("lang_check_test_config_precedence");
586 let _ = std::fs::remove_dir_all(&dir);
587 std::fs::create_dir_all(&dir).unwrap();
588
589 std::fs::write(
591 dir.join(".languagecheck.yaml"),
592 "engines:\n harper: false\n",
593 )
594 .unwrap();
595 std::fs::write(
596 dir.join(".languagecheck.json"),
597 r#"{"engines": {"harper": true}}"#,
598 )
599 .unwrap();
600
601 let config = Config::load(&dir).unwrap();
602 assert!(!config.engines.harper.enabled);
604
605 let _ = std::fs::remove_dir_all(&dir);
606 }
607
608 #[test]
609 fn load_missing_file_returns_default() {
610 let dir = std::env::temp_dir().join("lang_check_test_config_missing");
611 let _ = std::fs::remove_dir_all(&dir);
612 std::fs::create_dir_all(&dir).unwrap();
613
614 let config = Config::load(&dir).unwrap();
615 assert!(config.engines.harper.enabled);
616
617 let _ = std::fs::remove_dir_all(&dir);
618 }
619
620 #[test]
621 fn auto_fix_simple_replacement() {
622 let config = Config {
623 auto_fix: vec![AutoFixRule {
624 find: "teh".to_string(),
625 replace: "the".to_string(),
626 context: None,
627 description: None,
628 }],
629 ..Config::default()
630 };
631 let (result, count) = config.apply_auto_fixes("Fix teh typo in teh text.");
632 assert_eq!(result, "Fix the typo in the text.");
633 assert_eq!(count, 2);
634 }
635
636 #[test]
637 fn auto_fix_with_context_filter() {
638 let config = Config {
639 auto_fix: vec![AutoFixRule {
640 find: "colour".to_string(),
641 replace: "color".to_string(),
642 context: Some("American".to_string()),
643 description: Some("Use American spelling".to_string()),
644 }],
645 ..Config::default()
646 };
647 let (result, count) = config.apply_auto_fixes("American English: the colour is red.");
649 assert_eq!(result, "American English: the color is red.");
650 assert_eq!(count, 1);
651
652 let (result, count) = config.apply_auto_fixes("British English: the colour is red.");
654 assert_eq!(result, "British English: the colour is red.");
655 assert_eq!(count, 0);
656 }
657
658 #[test]
659 fn auto_fix_no_match() {
660 let config = Config {
661 auto_fix: vec![AutoFixRule {
662 find: "foo".to_string(),
663 replace: "bar".to_string(),
664 context: None,
665 description: None,
666 }],
667 ..Config::default()
668 };
669 let (result, count) = config.apply_auto_fixes("No matches here.");
670 assert_eq!(result, "No matches here.");
671 assert_eq!(count, 0);
672 }
673
674 #[test]
675 fn auto_fix_multiple_rules() {
676 let config = Config {
677 auto_fix: vec![
678 AutoFixRule {
679 find: "recieve".to_string(),
680 replace: "receive".to_string(),
681 context: None,
682 description: None,
683 },
684 AutoFixRule {
685 find: "seperate".to_string(),
686 replace: "separate".to_string(),
687 context: None,
688 description: None,
689 },
690 ],
691 ..Config::default()
692 };
693 let (result, count) = config.apply_auto_fixes("Please recieve the seperate package.");
694 assert_eq!(result, "Please receive the separate package.");
695 assert_eq!(count, 2);
696 }
697
698 #[test]
699 fn auto_fix_loads_from_yaml() {
700 let yaml = r#"
701auto_fix:
702 - find: "teh"
703 replace: "the"
704 description: "Fix common typo"
705 - find: "colour"
706 replace: "color"
707 context: "American"
708"#;
709 let config: Config = serde_yaml::from_str(yaml).unwrap();
710 assert_eq!(config.auto_fix.len(), 2);
711 assert_eq!(config.auto_fix[0].find, "teh");
712 assert_eq!(config.auto_fix[0].replace, "the");
713 assert_eq!(
714 config.auto_fix[0].description.as_deref(),
715 Some("Fix common typo")
716 );
717 assert_eq!(config.auto_fix[1].context.as_deref(), Some("American"));
718 }
719
720 #[test]
721 fn default_config_has_empty_auto_fix() {
722 let config = Config::default();
723 assert!(config.auto_fix.is_empty());
724 }
725
726 #[test]
727 fn external_providers_from_yaml() {
728 let yaml = r#"
729engines:
730 harper: true
731 languagetool: false
732 external:
733 - name: vale
734 command: /usr/bin/vale
735 args: ["--output", "JSON"]
736 extensions: [md, rst]
737 - name: custom-checker
738 command: ./my-checker
739"#;
740 let config: Config = serde_yaml::from_str(yaml).unwrap();
741 assert_eq!(config.engines.external.len(), 2);
742 assert_eq!(config.engines.external[0].name, "vale");
743 assert_eq!(config.engines.external[0].command, "/usr/bin/vale");
744 assert_eq!(config.engines.external[0].args, vec!["--output", "JSON"]);
745 assert_eq!(config.engines.external[0].extensions, vec!["md", "rst"]);
746 assert_eq!(config.engines.external[1].name, "custom-checker");
747 assert!(config.engines.external[1].args.is_empty());
748 }
749
750 #[test]
751 fn default_config_has_no_external_providers() {
752 let config = Config::default();
753 assert!(config.engines.external.is_empty());
754 }
755
756 #[test]
757 fn wasm_plugins_from_yaml() {
758 let yaml = r#"
759engines:
760 harper: true
761 wasm_plugins:
762 - name: custom-checker
763 path: .languagecheck/plugins/checker.wasm
764 extensions: [md, html]
765 - name: style-linter
766 path: /opt/plugins/style.wasm
767"#;
768 let config: Config = serde_yaml::from_str(yaml).unwrap();
769 assert_eq!(config.engines.wasm_plugins.len(), 2);
770 assert_eq!(config.engines.wasm_plugins[0].name, "custom-checker");
771 assert_eq!(
772 config.engines.wasm_plugins[0].path,
773 ".languagecheck/plugins/checker.wasm"
774 );
775 assert_eq!(
776 config.engines.wasm_plugins[0].extensions,
777 vec!["md", "html"]
778 );
779 assert_eq!(config.engines.wasm_plugins[1].name, "style-linter");
780 assert!(config.engines.wasm_plugins[1].extensions.is_empty());
781 }
782
783 #[test]
784 fn default_config_has_no_wasm_plugins() {
785 let config = Config::default();
786 assert!(config.engines.wasm_plugins.is_empty());
787 }
788
789 #[test]
790 fn performance_config_defaults() {
791 let config = Config::default();
792 assert!(!config.performance.high_performance_mode);
793 assert_eq!(config.performance.debounce_ms, 300);
794 assert_eq!(config.performance.max_file_size, 0);
795 }
796
797 #[test]
798 fn performance_config_from_yaml() {
799 let yaml = r#"
800performance:
801 high_performance_mode: true
802 debounce_ms: 500
803 max_file_size: 1048576
804"#;
805 let config: Config = serde_yaml::from_str(yaml).unwrap();
806 assert!(config.performance.high_performance_mode);
807 assert_eq!(config.performance.debounce_ms, 500);
808 assert_eq!(config.performance.max_file_size, 1_048_576);
809 }
810
811 #[test]
812 fn latex_skip_environments_from_yaml() {
813 let yaml = r#"
814languages:
815 latex:
816 skip_environments:
817 - prooftree
818 - mycustomenv
819"#;
820 let config: Config = serde_yaml::from_str(yaml).unwrap();
821 assert_eq!(
822 config.languages.latex.skip_environments,
823 vec!["prooftree", "mycustomenv"]
824 );
825 }
826
827 #[test]
828 fn default_config_has_empty_latex_skip_environments() {
829 let config = Config::default();
830 assert!(config.languages.latex.skip_environments.is_empty());
831 }
832
833 #[test]
834 fn latex_skip_commands_from_yaml() {
835 let yaml = r#"
836languages:
837 latex:
838 skip_commands:
839 - codefont
840 - myverb
841"#;
842 let config: Config = serde_yaml::from_str(yaml).unwrap();
843 assert_eq!(
844 config.languages.latex.skip_commands,
845 vec!["codefont", "myverb"]
846 );
847 }
848
849 #[test]
850 fn default_spell_language_is_en_us() {
851 let config = Config::default();
852 assert_eq!(config.engines.spell_language, "en-US");
853 }
854
855 #[test]
856 fn spell_language_from_yaml() {
857 let yaml = r#"
858engines:
859 spell_language: de-DE
860"#;
861 let config: Config = serde_yaml::from_str(yaml).unwrap();
862 assert_eq!(config.engines.spell_language, "de-DE");
863 }
864
865 #[test]
866 fn default_config_has_empty_latex_skip_commands() {
867 let config = Config::default();
868 assert!(config.languages.latex.skip_commands.is_empty());
869 }
870
871 #[test]
872 fn default_vale_is_disabled() {
873 let config = Config::default();
874 assert!(!config.engines.vale.enabled);
875 assert!(config.engines.vale.config.is_none());
876 }
877
878 #[test]
879 fn vale_bool_shorthand_from_yaml() {
880 let yaml = r#"
881engines:
882 vale: true
883"#;
884 let config: Config = serde_yaml::from_str(yaml).unwrap();
885 assert!(config.engines.vale.enabled);
886 }
887
888 #[test]
889 fn vale_nested_config_from_yaml() {
890 let yaml = r#"
891engines:
892 vale:
893 enabled: true
894 config: ".vale.ini"
895"#;
896 let config: Config = serde_yaml::from_str(yaml).unwrap();
897 assert!(config.engines.vale.enabled);
898 assert_eq!(config.engines.vale.config.as_deref(), Some(".vale.ini"));
899 }
900
901 #[test]
902 fn harper_nested_config_from_yaml() {
903 let yaml = r#"
904engines:
905 harper:
906 enabled: true
907 dialect: "British"
908 linters:
909 LongSentences: false
910"#;
911 let config: Config = serde_yaml::from_str(yaml).unwrap();
912 assert!(config.engines.harper.enabled);
913 assert_eq!(config.engines.harper.dialect, "British");
914 assert_eq!(
915 config.engines.harper.linters.get("LongSentences"),
916 Some(&false)
917 );
918 }
919
920 #[test]
921 fn languagetool_nested_config_from_yaml() {
922 let yaml = r#"
923engines:
924 languagetool:
925 enabled: true
926 url: "http://localhost:9090"
927 level: "picky"
928 disabled_rules:
929 - WHITESPACE_RULE
930"#;
931 let config: Config = serde_yaml::from_str(yaml).unwrap();
932 assert!(config.engines.languagetool.enabled);
933 assert_eq!(config.engines.languagetool.url, "http://localhost:9090");
934 assert_eq!(config.engines.languagetool.level, "picky");
935 assert_eq!(
936 config.engines.languagetool.disabled_rules,
937 vec!["WHITESPACE_RULE"]
938 );
939 }
940
941 #[test]
942 fn default_proselint_is_disabled() {
943 let config = Config::default();
944 assert!(!config.engines.proselint.enabled);
945 assert!(config.engines.proselint.config.is_none());
946 }
947
948 #[test]
949 fn proselint_bool_shorthand_from_yaml() {
950 let yaml = r#"
951engines:
952 proselint: true
953"#;
954 let config: Config = serde_yaml::from_str(yaml).unwrap();
955 assert!(config.engines.proselint.enabled);
956 }
957
958 #[test]
959 fn proselint_nested_config_from_yaml() {
960 let yaml = r#"
961engines:
962 proselint:
963 enabled: true
964 config: "proselint.json"
965"#;
966 let config: Config = serde_yaml::from_str(yaml).unwrap();
967 assert!(config.engines.proselint.enabled);
968 assert_eq!(
969 config.engines.proselint.config.as_deref(),
970 Some("proselint.json")
971 );
972 }
973}