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 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 #[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 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}