Skip to main content

lang_check/
rules.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Deserialize)]
5pub struct RuleMapping {
6    pub provider: String,
7    pub mappings: Vec<MappingEntry>,
8}
9
10#[derive(Debug, Deserialize)]
11pub struct MappingEntry {
12    pub native_id: String,
13    pub unified_id: String,
14}
15
16pub struct RuleNormalizer {
17    mappings: HashMap<String, HashMap<String, String>>,
18}
19
20impl Default for RuleNormalizer {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl RuleNormalizer {
27    #[must_use]
28    pub fn new() -> Self {
29        let mut normalizer = Self {
30            mappings: HashMap::new(),
31        };
32
33        // Load default mappings
34        normalizer.load_defaults();
35
36        normalizer
37    }
38
39    fn load_defaults(&mut self) {
40        const HARPER_YAML: &str = include_str!("../data/harper_mapping.yaml");
41        const LT_YAML: &str = include_str!("../data/languagetool_mapping.yaml");
42
43        for yaml_src in [HARPER_YAML, LT_YAML] {
44            let mapping: RuleMapping =
45                serde_yaml::from_str(yaml_src).expect("embedded YAML mapping should be valid");
46            let mut map = HashMap::new();
47            for entry in mapping.mappings {
48                map.insert(entry.native_id, entry.unified_id);
49            }
50            self.mappings.insert(mapping.provider, map);
51        }
52    }
53
54    /// Returns all (provider, native\_id, unified\_id) triples, sorted for stable output.
55    #[must_use]
56    pub fn all_mappings(&self) -> Vec<(String, String, String)> {
57        let mut result = Vec::new();
58        for (provider, map) in &self.mappings {
59            for (native, unified) in map {
60                result.push((provider.clone(), native.clone(), unified.clone()));
61            }
62        }
63        result.sort();
64        result
65    }
66
67    #[must_use]
68    pub fn normalize(&self, provider: &str, native_id: &str) -> String {
69        if let Some(provider_mappings) = self.mappings.get(provider)
70            && let Some(unified_id) = provider_mappings.get(native_id)
71        {
72            return unified_id.clone();
73        }
74
75        // Default to a generic category if no mapping exists
76        if native_id.contains("spell") {
77            "spelling.unknown".to_string()
78        } else if native_id.contains("grammar") {
79            "grammar.unknown".to_string()
80        } else {
81            "style.unknown".to_string()
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn normalize_harper_spelling() {
92        let normalizer = RuleNormalizer::new();
93        assert_eq!(
94            normalizer.normalize("harper", "harper.Spelling"),
95            "spelling.typo"
96        );
97        assert_eq!(
98            normalizer.normalize("harper", "harper.Typo"),
99            "spelling.typo"
100        );
101    }
102
103    #[test]
104    fn normalize_lt_spelling() {
105        let normalizer = RuleNormalizer::new();
106        assert_eq!(
107            normalizer.normalize("languagetool", "languagetool.MORFOLOGIK_RULE_EN_US"),
108            "spelling.typo"
109        );
110        assert_eq!(
111            normalizer.normalize("languagetool", "languagetool.MORFOLOGIK_RULE_EN_GB"),
112            "spelling.typo"
113        );
114    }
115
116    #[test]
117    fn normalize_article_rules() {
118        let normalizer = RuleNormalizer::new();
119        assert_eq!(
120            normalizer.normalize("harper", "harper.AnA"),
121            "grammar.article"
122        );
123        assert_eq!(
124            normalizer.normalize("languagetool", "languagetool.EN_A_VS_AN"),
125            "grammar.article"
126        );
127    }
128
129    #[test]
130    fn normalize_agreement_rules() {
131        let normalizer = RuleNormalizer::new();
132        assert_eq!(
133            normalizer.normalize("harper", "harper.Agreement"),
134            "grammar.agreement"
135        );
136        assert_eq!(
137            normalizer.normalize("languagetool", "languagetool.SUBJECT_VERB_AGREEMENT"),
138            "grammar.agreement"
139        );
140    }
141
142    #[test]
143    fn normalize_style_rules() {
144        let normalizer = RuleNormalizer::new();
145        assert_eq!(
146            normalizer.normalize("harper", "harper.Readability"),
147            "style.readability"
148        );
149        assert_eq!(
150            normalizer.normalize("harper", "harper.WordChoice"),
151            "style.word_choice"
152        );
153        assert_eq!(
154            normalizer.normalize("languagetool", "languagetool.PASSIVE_VOICE"),
155            "style.passive_voice"
156        );
157    }
158
159    #[test]
160    fn normalize_typography_rules() {
161        let normalizer = RuleNormalizer::new();
162        assert_eq!(
163            normalizer.normalize("harper", "harper.Punctuation"),
164            "typography.punctuation"
165        );
166        assert_eq!(
167            normalizer.normalize("harper", "harper.Capitalization"),
168            "typography.capitalization"
169        );
170        assert_eq!(
171            normalizer.normalize("languagetool", "languagetool.DOUBLE_PUNCTUATION"),
172            "typography.punctuation"
173        );
174    }
175
176    #[test]
177    fn normalize_unknown_spelling_rule() {
178        let normalizer = RuleNormalizer::new();
179        assert_eq!(
180            normalizer.normalize("harper", "harper.SomeSpellRule_spell"),
181            "spelling.unknown"
182        );
183    }
184
185    #[test]
186    fn normalize_unknown_grammar_rule() {
187        let normalizer = RuleNormalizer::new();
188        assert_eq!(
189            normalizer.normalize("harper", "harper.SomeGrammarCheck_grammar"),
190            "grammar.unknown"
191        );
192    }
193
194    #[test]
195    fn normalize_completely_unknown_rule() {
196        let normalizer = RuleNormalizer::new();
197        assert_eq!(
198            normalizer.normalize("unknown_provider", "some.random.rule"),
199            "style.unknown"
200        );
201    }
202}