1use std::sync::OnceLock;
25
26use crate::form::Form;
27use crate::reader::read_forms;
28
29#[derive(Clone, Debug, PartialEq)]
32pub struct Refusal {
33 pub id: String,
34 pub number: i64,
35 pub source: String,
36 pub text: String,
37}
38
39impl Refusal {
40 pub fn with_suffix(mut self, suffix: &str) -> Refusal {
43 self.text.push_str(suffix);
44 self
45 }
46}
47
48impl std::fmt::Display for Refusal {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(&self.text)
51 }
52}
53
54struct Entry {
55 id: String,
56 number: i64,
57 source: String,
58 template: String,
59}
60
61pub struct Catalogue {
63 entries: Vec<Entry>,
64}
65
66impl Catalogue {
67 pub fn parse(text: &str) -> Catalogue {
71 let text = text.replace("\r\n", "\n");
74 let forms = match read_forms(&text) {
75 Ok(forms) => forms,
76 Err(_) => {
77 return Catalogue {
78 entries: Vec::new(),
79 }
80 }
81 };
82 let mut entries = Vec::with_capacity(forms.len());
83 for form in &forms {
84 let Form::List(l) = form else { continue };
85 if !l.head_is("message") || l.items.len() != 5 {
86 continue;
87 }
88 let (Form::Sym(id), Form::Sym(number), Form::Str(source), Form::Str(template)) =
89 (&l.items[1], &l.items[2], &l.items[3], &l.items[4])
90 else {
91 continue;
92 };
93 let Ok(number) = number.parse::<i64>() else {
94 continue;
95 };
96 entries.push(Entry {
97 id: id.clone(),
98 number,
99 source: source.clone(),
100 template: template.clone(),
101 });
102 }
103 Catalogue { entries }
104 }
105
106 pub fn count(&self) -> usize {
108 self.entries.len()
109 }
110
111 pub fn ids(&self) -> Vec<&str> {
113 self.entries.iter().map(|e| e.id.as_str()).collect()
114 }
115
116 pub fn template(&self, id: &str) -> Option<&str> {
119 self.entries
120 .iter()
121 .find(|e| e.id == id)
122 .map(|e| e.template.as_str())
123 }
124
125 pub fn raise(&self, id: &str, slots: &[(&str, &str)]) -> Option<Refusal> {
131 let entry = self.entries.iter().find(|e| e.id == id)?;
132 Some(match substitute_slots(&entry.template, slots) {
133 Ok(text) => Refusal {
134 id: id.to_string(),
135 number: entry.number,
136 source: entry.source.clone(),
137 text,
138 },
139 Err(slot) => internal(
140 id,
141 &format!("RaiseMsg: no value supplied for slot '{{{slot}}}'"),
142 ),
143 })
144 }
145}
146
147const CATALOGUE_TEXT: &str = include_str!("../data/messages.vla");
151
152pub fn catalogue() -> &'static Catalogue {
154 static CATALOGUE: OnceLock<Catalogue> = OnceLock::new();
155 CATALOGUE.get_or_init(|| Catalogue::parse(CATALOGUE_TEXT))
156}
157
158pub fn count() -> usize {
160 catalogue().count()
161}
162
163pub fn template(id: &str) -> Option<&'static str> {
166 catalogue().template(id)
167}
168
169pub fn ids() -> Vec<&'static str> {
171 catalogue().ids()
172}
173
174pub fn raise(id: &str, slots: &[(&str, &str)]) -> Refusal {
179 catalogue().raise(id, slots).unwrap_or_else(|| unknown(id))
180}
181
182pub fn unknown(id: &str) -> Refusal {
186 internal(id, &format!("RaiseMsg: unknown message id '{id}'"))
187}
188
189fn internal(id: &str, text: &str) -> Refusal {
192 Refusal {
193 id: id.to_string(),
194 number: 5,
195 source: "VLA-Messages".to_string(),
196 text: text.to_string(),
197 }
198}
199
200fn substitute_slots(template: &str, slots: &[(&str, &str)]) -> Result<String, String> {
204 let mut out = String::with_capacity(template.len());
205 let mut rest = template;
206 loop {
207 let Some(open) = rest.find('{') else {
208 out.push_str(rest);
209 return Ok(out);
210 };
211 let Some(close_rel) = rest[open..].find('}') else {
212 out.push_str(rest);
213 return Ok(out);
214 };
215 let close = open + close_rel;
216 out.push_str(&rest[..open]);
217 let name = &rest[open + 1..close];
218 match slots.iter().find(|(k, _)| *k == name) {
219 Some((_, v)) => out.push_str(v),
220 None => return Err(name.to_string()),
221 }
222 rest = &rest[close + 1..];
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn the_language_s_catalogue_is_read_whole() {
232 assert!(count() >= 176, "{} entries read", count());
236 assert_eq!(ids()[0], "vla-source-not-found");
237 assert_eq!(
238 template("vla-unbalanced-parens").unwrap(),
239 "unbalanced parentheses: missing ')'{loc}"
240 );
241 assert!(template("view-sheet-unknown").is_some());
242 assert!(template("english-test-failed").is_none());
244 let r = raise(
246 "vla-interpreter-only-handler",
247 &[("name", "on:sheet-change")],
248 );
249 assert_eq!(r.number, -2147221504 + 7001);
250 assert_eq!(r.source, "VLA");
251 for id in ids() {
253 let family = id.split('-').next().unwrap();
254 assert!(
255 ["vla", "interp", "lint", "view", "calc", "grid"].contains(&family),
256 "{id} is not the language's"
257 );
258 }
259 assert_eq!(
263 ids().iter().filter(|id| id.starts_with("grid-")).count(),
264 12
265 );
266 for id in ids().iter().filter(|id| id.starts_with("grid-")) {
269 let text = template(id).unwrap();
270 assert!(!text.contains("{line}"), "{id}: {text}");
271 }
272 for id in [
273 "view-projection-unknown",
274 "view-window-too-large",
275 "calc-construct-not-read",
276 ] {
277 assert!(template(id).is_some(), "{id}");
278 }
279 }
280
281 #[test]
282 fn raise_fills_slots_as_raisemsg_does() {
283 let r = raise("vla-form-missing-element", &[("n", "3")]);
284 assert_eq!(r.id, "vla-form-missing-element");
285 assert_eq!(r.number, 5);
286 assert_eq!(r.source, "VLA");
287 assert_eq!(r.text, "form is missing required element 3");
288 let r = raise("vla-unbalanced-parens", &[("loc", "")]);
289 assert_eq!(r.text, "unbalanced parentheses: missing ')'");
290 let r = raise(
291 "vla-expected-list-at-position",
292 &[("n", "2"), ("value", "x")],
293 );
294 assert_eq!(r.text, "expected a list at position 2, got 'x'");
295 }
296
297 #[test]
298 fn the_two_internal_errors_are_refusals_too() {
299 let r = raise("no-such-id", &[]);
300 assert_eq!(r.number, 5);
301 assert_eq!(r.source, "VLA-Messages");
302 assert_eq!(r.text, "RaiseMsg: unknown message id 'no-such-id'");
303 let r = raise("vla-form-missing-element", &[]);
304 assert_eq!(r.text, "RaiseMsg: no value supplied for slot '{n}'");
305 assert!(catalogue().raise("no-such-id", &[]).is_none());
308 assert!(catalogue().raise("vla-form-missing-element", &[]).is_some());
309 }
310
311 #[test]
312 fn substitute_slots_copies_an_unclosed_brace() {
313 assert_eq!(
314 substitute_slots("a {x} b {", &[("x", "1")]).unwrap(),
315 "a 1 b {"
316 );
317 assert_eq!(substitute_slots("no slots", &[]).unwrap(), "no slots");
318 assert_eq!(
319 substitute_slots("{a}{b}", &[("a", "1"), ("b", "2")]).unwrap(),
320 "12"
321 );
322 }
323}