1pub const ENGLISH_WORD_NOT_LEARNED: &str = "English word could not be learned.";
5pub const FREQUENCY_NOT_PERSISTED: &str = "Unable to persist candidate frequency adjustment.";
6pub const ENGLISH_FREQUENCY_NOT_PERSISTED: &str =
7 "English candidate frequency could not be persisted.";
8pub const PIN_NOT_PERSISTED: &str = "Unable to persist the pinned candidate.";
9pub const REMOVAL_NOT_PERSISTED: &str = "Unable to persist candidate removal.";
10pub const POSITION_NOT_PERSISTED: &str = "Unable to persist candidate position.";
11pub const PHRASE_NOT_PERSISTED: &str = "Unable to persist the composed phrase.";
12pub const SENTENCE_NOT_PERSISTED: &str = "Unable to persist the selected sentence.";
13pub const TYPO_NOT_PERSISTED: &str = "Typo correction could not be persisted.";
14pub const AUTOCORRECT_PREFERENCE_NOT_PERSISTED: &str =
15 "Autocorrect preference could not be persisted.";
16pub const PICK_TRANSITION_NOT_PERSISTED: &str = "Unable to persist the pick transition.";
17pub const PERSONAL_CONTEXT_NOT_PERSISTED: &str = "Unable to persist personal context.";
18pub const NINE_KEY_FREQUENCY_NOT_PERSISTED: &str =
19 "Unable to persist nine-key candidate frequency adjustment.";
20pub const NINE_KEY_REMOVAL_NOT_PERSISTED: &str = "Unable to persist nine-key candidate removal.";
21pub const NINE_KEY_POSITION_NOT_PERSISTED: &str = "Unable to persist nine-key candidate position.";
22pub const NINE_KEY_DIGIT_LIMIT: &str = "一次最多输入 48 个按键,请先选择候选";
23
24pub const QUICK_PHRASE_UNAVAILABLE: &str = "Quick phrase database is unavailable.";
26pub const QUICK_PHRASE_QUERY_FAILED: &str = "Quick phrase database could not be queried.";
27pub const EMOJI_UNAVAILABLE: &str = "Emoji database is unavailable.";
28pub const EMOJI_QUERY_FAILED: &str = "Emoji database could not be queried.";
29pub const KAOMOJI_UNAVAILABLE: &str = "Kaomoji database is unavailable.";
30pub const KAOMOJI_QUERY_FAILED: &str = "Kaomoji database could not be queried.";
31pub const SUPER_JIANPIN_UNAVAILABLE: &str = "Super-jianpin database is unavailable.";
32pub const SUPER_JIANPIN_QUERY_FAILED: &str = "Super-jianpin database could not be queried.";
33
34pub const INVALID_SESSION_OPTIONS: &str = "Invalid session options";
36pub const UNKNOWN_HELPCODE_SCHEMA: &str = "Unknown helpcode schema";
37pub const INVALID_PUNCTUATION_LOCK: &str = "Invalid punctuation lock";
38pub const RUNTIME_DIRECTORIES_MUST_BE_ABSOLUTE: &str = "Runtime directories must be absolute";
39pub const RUNTIME_ASSET_PATH_ESCAPES: &str = "Runtime asset path escapes its directory";
40pub const INVALID_RUNTIME_CONTENT_ID: &str = "Invalid runtime content ID";
41pub const RUNTIME_ROOTS_OVERLAP: &str =
42 "Resource, user-data and cache directories must not overlap";
43pub const INCOMPLETE_RUNTIME_GENERATION: &str = "Incomplete runtime dictionary generation";
44pub const RUNTIME_REPLAY_FAILED: &str = "Runtime dictionary replay failed: ";
46pub const RUNTIME_COPY_FAILED: &str = "Unable to copy runtime dictionary: ";
48pub const RUNTIME_FINALIZE_FAILED: &str = "Cannot finalize runtime dictionary generation";
49pub const DICTIONARY_FORMAT_UNSUPPORTED: &str = "Dictionary format version unsupported: ";
51pub const INVALID_DICTIONARY_STATE: &str = "Invalid or unavailable dictionary state";
52pub const DICTIONARY_MAINTENANCE_BUSY: &str = "Dictionary maintenance busy";
54
55pub const CHARACTER_MUST_BE_ASCII: &str = "Engine character must be ASCII";
57pub const PUNCTUATION_MUST_BE_ASCII: &str = "Engine punctuation must be ASCII";
58pub const PAIRED_OPENING_MUST_BE_ASCII: &str = "Paired punctuation opening must be ASCII";
59pub const UNSUPPORTED_INPUT_COMMAND: &str = "Unsupported input command";
60pub const INVALID_CANDIDATE_POSITION: &str = "Invalid candidate position";
61pub const INVALID_CANDIDATE_EDGE: &str = "Invalid candidate edge";
62pub const UNSUPPORTED_INPUT_SCHEME: &str = "Unsupported input scheme";
63pub const INPUT_SCHEME_NOT_ENABLED: &str = "Input scheme is not enabled";
65pub const UNSUPPORTED_SHUANGPIN_PROFILE: &str = "Unsupported shuangpin profile";
66pub const INVALID_CUSTOM_SHUANGPIN_PROFILE: &str = "Invalid custom shuangpin profile";
68pub const UNSUPPORTED_WUBI_PROFILE: &str = "Unsupported wubi profile";
69pub const UNSUPPORTED_FREQUENCY_MODE: &str = "Unsupported frequency mode";
70pub const UNSUPPORTED_VIETNAMESE_METHOD: &str = "Unsupported Vietnamese input method";
71pub const UNSUPPORTED_VIETNAMESE_TONE_STYLE: &str = "Unsupported Vietnamese tone style";
72pub const UNSUPPORTED_KOREAN_LAYOUT: &str = "Unsupported Korean layout";
73pub const UNSUPPORTED_DICTIONARY_KIND: &str = "Unsupported dictionary kind";
74pub const EXPECTED_AT_MOST_ONE_ENTRY: &str = "Expected at most one dictionary entry";
75pub const EXPECTED_AT_MOST_ONE_WEIGHT: &str = "Expected at most one weight";
76pub const TRANSLATION_SIDECAR_FAILED: &str = "Unable to prepare custom translation sidecar";
77pub const RESET_IN_PLACE: &str = "Cannot reset packaged dictionaries in place";
78pub const PACKAGED_DICTIONARY_UNAVAILABLE: &str = "Packaged dictionary is unavailable";
79pub const RESET_JOURNAL_FAILED: &str = "Cannot prepare empty learning journal";
80pub const RESET_STAGE_DICTIONARIES_FAILED: &str = "Cannot stage fresh dictionaries";
81pub const RESET_STAGE_FAILED: &str = "Cannot stage learned-data reset";
82pub const RESET_PUBLISH_FAILED: &str = "Cannot publish learned-data reset";
83pub const INVALID_SNAPSHOT_RECORD_LIMIT: &str = "Invalid snapshot record limit";
84pub const INVALID_SNAPSHOT_RECORD_TYPE: &str = "Invalid snapshot record type";
85pub const INVALID_SNAPSHOT_DICTIONARY_KIND: &str = "Invalid snapshot dictionary kind";
86pub const INVALID_SNAPSHOT_POSITION: &str = "Invalid snapshot position";
87pub const INVALID_SNAPSHOT_SELECTION_COUNT: &str = "Invalid snapshot selection count";
88pub const SNAPSHOT_STREAM_FAILED: &str = "Snapshot record stream failed";
89pub const ENGLISH_DICTIONARY_UNAVAILABLE: &str = "English dictionary unavailable";
90pub const INVALID_ENGLISH_COMPLETION_LIMIT: &str = "Invalid English completion limit";
91pub const INVALID_ENGLISH_COMPLETION_PREFIX: &str = "Invalid English completion prefix";
92pub const CANDIDATE_GLOSS_UNAVAILABLE: &str = "Candidate gloss dictionary unavailable";
93pub const OFFLINE_GLOSS_UNAVAILABLE: &str = "Offline gloss dictionary unavailable";
94pub const OFFLINE_GLOSS_UNREADABLE: &str = "Offline gloss dictionary unreadable";
95pub const OFFLINE_GLOSS_VERSION_UNSUPPORTED: &str = "Offline gloss dictionary version unsupported";
96pub const OFFLINE_GLOSS_LANGUAGE_MISMATCH: &str = "Offline gloss dictionary language mismatch";
97pub const OFFLINE_GLOSS_READ_FAILED: &str = "Offline gloss dictionary read failed";
98pub const PRONUNCIATION_UNAVAILABLE: &str = "Pronunciation dictionary unavailable";
99pub const PRONUNCIATION_UNREADABLE: &str = "Pronunciation dictionary unreadable";
100pub const PRONUNCIATION_VERSION_UNSUPPORTED: &str = "Pronunciation dictionary version unsupported";
101pub const PRONUNCIATION_KIND_MISMATCH: &str = "Pronunciation dictionary kind mismatch";
102pub const PRONUNCIATION_READ_FAILED: &str = "Pronunciation dictionary read failed";
103pub const LANGUAGE_DICTIONARY_UNAVAILABLE: &str = "Language dictionary unavailable";
104pub const LANGUAGE_DICTIONARY_VERSION_UNSUPPORTED: &str = "Language dictionary version unsupported";
105pub const INVALID_EMOJI_CATALOG_PAGE: &str = "Invalid emoji catalog page";
106pub const INVALID_EMOJI_CATALOG_CURSOR: &str = "Invalid emoji catalog cursor";
107pub const EMOJI_CATALOG_UNAVAILABLE: &str = "Emoji catalog unavailable";
108pub const EMOJI_CATALOG_QUERY_UNAVAILABLE: &str = "Emoji catalog query unavailable";
109pub const EMOJI_CATALOG_QUERY_REJECTED: &str = "Emoji catalog query rejected";
110pub const EMOJI_CATALOG_READ_FAILED: &str = "Emoji catalog read failed";
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116#[repr(u16)]
117pub enum DiagnosticCode {
118 EnglishWordNotLearned = 1,
119 FrequencyNotPersisted = 2,
120 EnglishFrequencyNotPersisted = 3,
121 PinNotPersisted = 4,
122 RemovalNotPersisted = 5,
123 PositionNotPersisted = 6,
124 PhraseNotPersisted = 7,
125 SentenceNotPersisted = 8,
126 TypoNotPersisted = 9,
127 AutocorrectPreferenceNotPersisted = 10,
128 PickTransitionNotPersisted = 11,
129 PersonalContextNotPersisted = 12,
130 NineKeyFrequencyNotPersisted = 13,
131 NineKeyRemovalNotPersisted = 14,
132 NineKeyPositionNotPersisted = 15,
133 NineKeyDigitLimit = 16,
134 QuickPhraseUnavailable = 17,
135 QuickPhraseQueryFailed = 18,
136 EmojiUnavailable = 19,
137 EmojiQueryFailed = 20,
138 KaomojiUnavailable = 21,
139 KaomojiQueryFailed = 22,
140 SuperJianpinUnavailable = 23,
141 SuperJianpinQueryFailed = 24,
142 LanguageDictionaryUnavailable = 25,
144 Other = 0xFFFF,
146}
147
148pub const DIAGNOSTIC_CODES: [(DiagnosticCode, &str); 25] = [
150 (
151 DiagnosticCode::EnglishWordNotLearned,
152 ENGLISH_WORD_NOT_LEARNED,
153 ),
154 (
155 DiagnosticCode::FrequencyNotPersisted,
156 FREQUENCY_NOT_PERSISTED,
157 ),
158 (
159 DiagnosticCode::EnglishFrequencyNotPersisted,
160 ENGLISH_FREQUENCY_NOT_PERSISTED,
161 ),
162 (DiagnosticCode::PinNotPersisted, PIN_NOT_PERSISTED),
163 (DiagnosticCode::RemovalNotPersisted, REMOVAL_NOT_PERSISTED),
164 (DiagnosticCode::PositionNotPersisted, POSITION_NOT_PERSISTED),
165 (DiagnosticCode::PhraseNotPersisted, PHRASE_NOT_PERSISTED),
166 (DiagnosticCode::SentenceNotPersisted, SENTENCE_NOT_PERSISTED),
167 (DiagnosticCode::TypoNotPersisted, TYPO_NOT_PERSISTED),
168 (
169 DiagnosticCode::AutocorrectPreferenceNotPersisted,
170 AUTOCORRECT_PREFERENCE_NOT_PERSISTED,
171 ),
172 (
173 DiagnosticCode::PickTransitionNotPersisted,
174 PICK_TRANSITION_NOT_PERSISTED,
175 ),
176 (
177 DiagnosticCode::PersonalContextNotPersisted,
178 PERSONAL_CONTEXT_NOT_PERSISTED,
179 ),
180 (
181 DiagnosticCode::NineKeyFrequencyNotPersisted,
182 NINE_KEY_FREQUENCY_NOT_PERSISTED,
183 ),
184 (
185 DiagnosticCode::NineKeyRemovalNotPersisted,
186 NINE_KEY_REMOVAL_NOT_PERSISTED,
187 ),
188 (
189 DiagnosticCode::NineKeyPositionNotPersisted,
190 NINE_KEY_POSITION_NOT_PERSISTED,
191 ),
192 (DiagnosticCode::NineKeyDigitLimit, NINE_KEY_DIGIT_LIMIT),
193 (
194 DiagnosticCode::QuickPhraseUnavailable,
195 QUICK_PHRASE_UNAVAILABLE,
196 ),
197 (
198 DiagnosticCode::QuickPhraseQueryFailed,
199 QUICK_PHRASE_QUERY_FAILED,
200 ),
201 (DiagnosticCode::EmojiUnavailable, EMOJI_UNAVAILABLE),
202 (DiagnosticCode::EmojiQueryFailed, EMOJI_QUERY_FAILED),
203 (DiagnosticCode::KaomojiUnavailable, KAOMOJI_UNAVAILABLE),
204 (DiagnosticCode::KaomojiQueryFailed, KAOMOJI_QUERY_FAILED),
205 (
206 DiagnosticCode::SuperJianpinUnavailable,
207 SUPER_JIANPIN_UNAVAILABLE,
208 ),
209 (
210 DiagnosticCode::SuperJianpinQueryFailed,
211 SUPER_JIANPIN_QUERY_FAILED,
212 ),
213 (
214 DiagnosticCode::LanguageDictionaryUnavailable,
215 LANGUAGE_DICTIONARY_UNAVAILABLE,
216 ),
217];
218
219impl DiagnosticCode {
220 pub const fn value(self) -> u16 {
221 self as u16
222 }
223
224 pub fn from_message(message: &str) -> Self {
226 DIAGNOSTIC_CODES
227 .iter()
228 .find(|(_, text)| *text == message)
229 .map_or(Self::Other, |(code, _)| *code)
230 }
231
232 pub fn message(self) -> Option<&'static str> {
234 DIAGNOSTIC_CODES
235 .iter()
236 .find(|(code, _)| *code == self)
237 .map(|(_, text)| *text)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
247 fn diagnostic_codes_are_frozen_and_round_trip() {
248 for (index, (code, message)) in DIAGNOSTIC_CODES.iter().enumerate() {
249 assert_eq!(usize::from(code.value()), index + 1, "{message}");
250 assert_eq!(DiagnosticCode::from_message(message), *code);
251 assert_eq!(code.message(), Some(*message));
252 }
253 assert_eq!(DiagnosticCode::Other.value(), 0xFFFF);
254 assert_eq!(DiagnosticCode::Other.message(), None);
255 assert_eq!(
256 DiagnosticCode::from_message("database is locked"),
257 DiagnosticCode::Other
258 );
259 assert_eq!(
260 DiagnosticCode::from_message(NINE_KEY_DIGIT_LIMIT).value(),
261 16
262 );
263 assert_eq!(
264 DiagnosticCode::from_message(SUPER_JIANPIN_QUERY_FAILED).value(),
265 24
266 );
267 assert_eq!(
268 DiagnosticCode::from_message(LANGUAGE_DICTIONARY_UNAVAILABLE).value(),
269 25
270 );
271 }
272
273 fn constants() -> Vec<(&'static str, &'static str)> {
275 let source = include_str!("diagnostics.rs");
276 let production = &source[..source.find("// ---- 诊断码").unwrap()];
277 production
278 .split("pub const ")
279 .skip(1)
280 .map(|item| {
281 let name = &item[..item.find(':').unwrap()];
282 let literal = &item[item.find('"').unwrap() + 1..];
283 (name, &literal[..literal.find('"').unwrap()])
284 })
285 .collect()
286 }
287
288 #[test]
290 fn every_key_result_diagnostic_constant_has_a_code() {
291 let source = include_str!("diagnostics.rs");
292 let start = source.find("// ---- KeyResult diagnostics").unwrap();
293 let end = source.find("// ---- Engine errors").unwrap();
294 let section = &source[start..end];
295 let mut checked = 0;
296 for (name, message) in constants() {
297 if section.contains(&format!("pub const {name}:")) {
298 assert_ne!(
299 DiagnosticCode::from_message(message),
300 DiagnosticCode::Other,
301 "{name}"
302 );
303 checked += 1;
304 }
305 }
306 assert_eq!(checked, 24);
307 }
308
309 #[test]
311 fn every_constant_passed_to_with_diagnostic_has_a_code() {
312 fn sources(directory: &std::path::Path, found: &mut Vec<std::path::PathBuf>) {
313 for entry in std::fs::read_dir(directory).unwrap() {
314 let path = entry.unwrap().path();
315 if path.is_dir() {
316 sources(&path, found);
317 } else if path.extension().is_some_and(|extension| extension == "rs") {
318 found.push(path);
319 }
320 }
321 }
322 fn argument(text: &str) -> &str {
324 let (mut depth, mut quoted, mut escaped) = (1, false, false);
325 for (index, character) in text.char_indices() {
326 match character {
327 _ if escaped => escaped = false,
328 '\\' if quoted => escaped = true,
329 '"' => quoted = !quoted,
330 '(' if !quoted => depth += 1,
331 ')' if !quoted => {
332 depth -= 1;
333 if depth == 0 {
334 return &text[..index];
335 }
336 }
337 _ => {}
338 }
339 }
340 panic!("unbalanced with_diagnostic call");
341 }
342
343 let constants = constants();
344 let mut files = Vec::new();
345 sources(
346 &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
347 &mut files,
348 );
349 let mut passed = std::collections::BTreeSet::new();
350 for path in files {
351 let source = std::fs::read_to_string(&path).unwrap();
352 let production = source.split("\n#[cfg(test)]\nmod tests").next().unwrap();
353 for (start, call) in production.match_indices("with_diagnostic(") {
354 let argument = argument(&production[start + call.len()..]);
355 for reference in argument.split("diagnostics::").skip(1) {
356 let name: String = reference
357 .chars()
358 .take_while(|character| {
359 character.is_ascii_uppercase()
360 || character.is_ascii_digit()
361 || *character == '_'
362 })
363 .collect();
364 if name.len() < 2 {
365 continue;
366 }
367 let message = constants
368 .iter()
369 .find(|(constant, _)| *constant == name)
370 .unwrap_or_else(|| panic!("{} uses unknown {name}", path.display()))
371 .1;
372 assert_ne!(
373 DiagnosticCode::from_message(message),
374 DiagnosticCode::Other,
375 "{} passes {name} to with_diagnostic without a code",
376 path.display()
377 );
378 passed.insert(name);
379 }
380 }
381 }
382 assert!(
383 passed.contains("LANGUAGE_DICTIONARY_UNAVAILABLE")
384 && passed.contains("NINE_KEY_DIGIT_LIMIT"),
385 "{passed:?}"
386 );
387 }
388}