Skip to main content

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