mongol_norm/error.rs
1//! The crate's single error type. `Display` wording mirrors the Python exceptions so the CLI
2//! prints the same messages as the Python CLI.
3
4use std::fmt;
5
6use crate::{UnitPosition, WrittenUnit};
7
8/// Everything that can go wrong in this crate.
9#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Error {
12 /// `shape()` / `normalize()` input contains a character that is not a Mongolian letter,
13 /// FVS, MVS, NNBSP, nirugu or ZWJ. `index` is a `char` index (not a byte offset).
14 NonMongolianChar {
15 /// The offending character.
16 ch: char,
17 /// Its `char` index in the input.
18 index: usize,
19 },
20 /// `FontShaper::shape`: a character of the Mongolian block the bundled Hudum font has no
21 /// glyph for — a letter of another writing system, an Ali Gali sign, an unassigned code
22 /// point. `index` is a `char` index.
23 UnsupportedChar {
24 /// The offending character.
25 ch: char,
26 /// Its `char` index in the input.
27 index: usize,
28 },
29 /// Strict normalization found no canonical encoding for the written-unit shape
30 /// (Python `NormalizationFallbackError`).
31 NormalizationFallback {
32 /// The input text.
33 text: String,
34 /// Its shape, which the normalize table could not encode.
35 written_units: Vec<WrittenUnit>,
36 },
37 /// `parse_written_units`: a name that is neither a unit known to the normalize table nor
38 /// `Mvs`/`Nirugu`/`Zwj`.
39 UnknownWrittenUnit {
40 /// Index of the unit in the parsed sequence.
41 index: usize,
42 /// The unparseable name.
43 unit: String,
44 },
45 /// `normalize_written_units`: a written unit this shaper's normalize table does not know.
46 UnsupportedWrittenUnit {
47 /// Index of the unit in the input sequence.
48 index: usize,
49 /// The unsupported unit.
50 unit: WrittenUnit,
51 },
52 /// `normalize_written_units`: the sequence has no canonical MNG encoding (or the candidate
53 /// does not reshape to exactly the requested sequence).
54 NoCanonicalEncoding,
55 /// `normalize_positioned_written_units`: explicit `Zwj` records are rejected.
56 ExplicitZwj,
57 /// `normalize_positioned_written_units`: `Mvs` / `Nirugu` need [`UnitPosition::Control`].
58 ControlRequiresControlPosition {
59 /// Index of the record.
60 index: usize,
61 /// The control unit.
62 unit: WrittenUnit,
63 },
64 /// `normalize_positioned_written_units`: `(unit, position)` is not in the HUD inventory
65 /// (a letter with `Control` lands here too).
66 UnsupportedPositionedUnit {
67 /// Index of the record.
68 index: usize,
69 /// The unit.
70 unit: WrittenUnit,
71 /// The requested position.
72 position: UnitPosition,
73 },
74 /// `normalize_positioned_written_units`: the records do not form a valid init…fina chain in
75 /// the supplied joining context.
76 ChainPositionMismatch,
77 /// `normalize_positioned_written_units`: more than the maximum number of records.
78 TooManyRecords {
79 /// The maximum accepted.
80 max: usize,
81 },
82 /// `parse_written_units`: empty unit, whitespace, or an ambiguous compact segmentation.
83 InvalidUnitSpec(String),
84 /// A contract name (`Locale`, `Position`, `WrittenUnit`, …) failed to parse.
85 UnknownName {
86 /// What kind of name was expected (`"locale"`, `"written unit"`, …).
87 kind: &'static str,
88 /// The rejected text.
89 name: String,
90 },
91}
92
93impl fmt::Display for Error {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 // `escape_debug` mirrors Python's `repr(ch)` for control characters only: a newline
97 // prints as `\n`, a printable character as itself. Quotes and non-printable
98 // non-ASCII are spelled differently — Rust escapes `'` as `\'` and writes `\u{85}`
99 // where Python switches the quote style and writes `\x85`.
100 Error::NonMongolianChar { ch, index } => write!(
101 f,
102 "non-Mongolian character '{}' (U+{:04X}) at index {}: shape() / normalize() \
103 accept only Mongolian letters + FVS/MVS/NNBSP/Nirugu/ZWJ. For mixed-script \
104 input use normalize_text().",
105 ch.escape_debug(),
106 *ch as u32,
107 index
108 ),
109 Error::UnsupportedChar { ch, index } => write!(
110 f,
111 "unsupported character '{}' (U+{:04X}) at index {}: the shaping font has no \
112 written form for it",
113 ch.escape_debug(),
114 *ch as u32,
115 index
116 ),
117 Error::NormalizationFallback { written_units, .. } => {
118 let names: Vec<&str> = written_units.iter().map(|u| u.as_str()).collect();
119 write!(
120 f,
121 "normalization fallback: no canonical encoding for written units {}",
122 names.join("+")
123 )
124 }
125 Error::UnknownWrittenUnit { index, unit } => {
126 // `unit` is arbitrary user text (Python renders it with `repr()`), so escape it —
127 // a control character must never reach a terminal raw. Printable ASCII, which
128 // every real unit name is, passes through unchanged. As above, the escaping
129 // matches Python's `repr()` for control characters only; quotes and
130 // non-printable non-ASCII use Rust's `\'` / `\u{…}` spelling instead.
131 write!(
132 f,
133 "written_units[{index}] is unknown: '{}'",
134 unit.escape_debug()
135 )
136 }
137 Error::UnsupportedWrittenUnit { index, unit } => {
138 write!(f, "written_units[{index}] is unknown: '{}'", unit.as_str())
139 }
140 Error::NoCanonicalEncoding => {
141 f.write_str("written-unit sequence has no canonical MNG encoding")
142 }
143 Error::ExplicitZwj => f.write_str("unsupported positioned control 'Zwj'"),
144 Error::ControlRequiresControlPosition { index, unit } => write!(
145 f,
146 "positioned_units[{index}] control '{}' requires position 'control'",
147 unit.as_str()
148 ),
149 Error::UnsupportedPositionedUnit { unit, position, .. } => write!(
150 f,
151 "unsupported positioned written unit '{}:{}'",
152 unit.as_str(),
153 position.as_str()
154 ),
155 Error::ChainPositionMismatch => f.write_str(
156 "positioned written-unit sequence has no canonical MNG encoding in the \
157 supplied context",
158 ),
159 Error::TooManyRecords { max } => {
160 write!(f, "positioned_units accepts at most {max} records")
161 }
162 Error::InvalidUnitSpec(message) => f.write_str(message),
163 Error::UnknownName { kind, name } => write!(f, "unknown {kind} '{name}'"),
164 }
165 }
166}
167
168impl std::error::Error for Error {}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 /// These strings are a byte-exact contract: later CLI tests assert on them directly, the
175 /// same way the Python CLI's own error messages are asserted on.
176 #[test]
177 fn display_wording_matches_python() {
178 assert_eq!(
179 Error::NonMongolianChar { ch: 'x', index: 3 }.to_string(),
180 "non-Mongolian character 'x' (U+0078) at index 3: shape() / normalize() accept only \
181 Mongolian letters + FVS/MVS/NNBSP/Nirugu/ZWJ. For mixed-script input use \
182 normalize_text()."
183 );
184 // Python's `{ch!r}` escapes a control character; so must ours (`repr('\n')` == r"'\n'").
185 assert_eq!(
186 Error::NonMongolianChar { ch: '\n', index: 4 }.to_string(),
187 "non-Mongolian character '\\n' (U+000A) at index 4: shape() / normalize() accept only \
188 Mongolian letters + FVS/MVS/NNBSP/Nirugu/ZWJ. For mixed-script input use \
189 normalize_text()."
190 );
191 // An unassigned code point is not printable, so `escape_debug` spells it out (as
192 // Python's `repr` would, with its own `\u181a` spelling).
193 assert_eq!(
194 Error::UnsupportedChar {
195 ch: '\u{181A}',
196 index: 2
197 }
198 .to_string(),
199 "unsupported character '\\u{181a}' (U+181A) at index 2: the shaping font has no \
200 written form for it"
201 );
202 assert_eq!(
203 Error::UnsupportedChar {
204 ch: '\u{1878}',
205 index: 0
206 }
207 .to_string(),
208 "unsupported character '\u{1878}' (U+1878) at index 0: the shaping font has no written \
209 form for it"
210 );
211 assert_eq!(
212 Error::NormalizationFallback {
213 text: "t".into(),
214 written_units: vec![
215 WrittenUnit::S,
216 WrittenUnit::A,
217 WrittenUnit::I,
218 WrittenUnit::I,
219 WrittenUnit::A,
220 ],
221 }
222 .to_string(),
223 "normalization fallback: no canonical encoding for written units S+A+I+I+A"
224 );
225 assert_eq!(
226 Error::UnknownWrittenUnit {
227 index: 1,
228 unit: "Unknown".into()
229 }
230 .to_string(),
231 "written_units[1] is unknown: 'Unknown'"
232 );
233 // A name is arbitrary user text; control characters are escaped, never emitted raw.
234 assert_eq!(
235 Error::UnknownWrittenUnit {
236 index: 0,
237 unit: "A\0B".into()
238 }
239 .to_string(),
240 "written_units[0] is unknown: 'A\\0B'"
241 );
242 assert_eq!(
243 Error::UnsupportedWrittenUnit {
244 index: 1,
245 unit: WrittenUnit::B
246 }
247 .to_string(),
248 "written_units[1] is unknown: 'B'"
249 );
250 assert_eq!(
251 Error::NoCanonicalEncoding.to_string(),
252 "written-unit sequence has no canonical MNG encoding"
253 );
254 assert_eq!(
255 Error::ExplicitZwj.to_string(),
256 "unsupported positioned control 'Zwj'"
257 );
258 assert_eq!(
259 Error::ControlRequiresControlPosition {
260 index: 0,
261 unit: WrittenUnit::Mvs
262 }
263 .to_string(),
264 "positioned_units[0] control 'Mvs' requires position 'control'"
265 );
266 assert_eq!(
267 Error::UnsupportedPositionedUnit {
268 index: 0,
269 unit: WrittenUnit::F,
270 position: UnitPosition::Isol,
271 }
272 .to_string(),
273 "unsupported positioned written unit 'F:isol'"
274 );
275 assert_eq!(
276 Error::ChainPositionMismatch.to_string(),
277 "positioned written-unit sequence has no canonical MNG encoding in the supplied \
278 context"
279 );
280 assert_eq!(
281 Error::TooManyRecords { max: 1024 }.to_string(),
282 "positioned_units accepts at most 1024 records"
283 );
284 assert_eq!(Error::InvalidUnitSpec("x".into()).to_string(), "x");
285 assert_eq!(
286 Error::UnknownName {
287 kind: "locale",
288 name: "XX".into()
289 }
290 .to_string(),
291 "unknown locale 'XX'"
292 );
293 }
294}