use std::sync::OnceLock;
use crate::form::Form;
use crate::reader::read_forms;
#[derive(Clone, Debug, PartialEq)]
pub struct Refusal {
pub id: String,
pub number: i64,
pub source: String,
pub text: String,
}
impl Refusal {
pub fn with_suffix(mut self, suffix: &str) -> Refusal {
self.text.push_str(suffix);
self
}
}
impl std::fmt::Display for Refusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.text)
}
}
struct Entry {
id: String,
number: i64,
source: String,
template: String,
}
pub struct Catalogue {
entries: Vec<Entry>,
}
impl Catalogue {
pub fn parse(text: &str) -> Catalogue {
let text = text.replace("\r\n", "\n");
let forms = match read_forms(&text) {
Ok(forms) => forms,
Err(_) => {
return Catalogue {
entries: Vec::new(),
}
}
};
let mut entries = Vec::with_capacity(forms.len());
for form in &forms {
let Form::List(l) = form else { continue };
if !l.head_is("message") || l.items.len() != 5 {
continue;
}
let (Form::Sym(id), Form::Sym(number), Form::Str(source), Form::Str(template)) =
(&l.items[1], &l.items[2], &l.items[3], &l.items[4])
else {
continue;
};
let Ok(number) = number.parse::<i64>() else {
continue;
};
entries.push(Entry {
id: id.clone(),
number,
source: source.clone(),
template: template.clone(),
});
}
Catalogue { entries }
}
pub fn count(&self) -> usize {
self.entries.len()
}
pub fn ids(&self) -> Vec<&str> {
self.entries.iter().map(|e| e.id.as_str()).collect()
}
pub fn template(&self, id: &str) -> Option<&str> {
self.entries
.iter()
.find(|e| e.id == id)
.map(|e| e.template.as_str())
}
pub fn raise(&self, id: &str, slots: &[(&str, &str)]) -> Option<Refusal> {
let entry = self.entries.iter().find(|e| e.id == id)?;
Some(match substitute_slots(&entry.template, slots) {
Ok(text) => Refusal {
id: id.to_string(),
number: entry.number,
source: entry.source.clone(),
text,
},
Err(slot) => internal(
id,
&format!("RaiseMsg: no value supplied for slot '{{{slot}}}'"),
),
})
}
}
const CATALOGUE_TEXT: &str = include_str!("../data/messages.vla");
pub fn catalogue() -> &'static Catalogue {
static CATALOGUE: OnceLock<Catalogue> = OnceLock::new();
CATALOGUE.get_or_init(|| Catalogue::parse(CATALOGUE_TEXT))
}
pub fn count() -> usize {
catalogue().count()
}
pub fn template(id: &str) -> Option<&'static str> {
catalogue().template(id)
}
pub fn ids() -> Vec<&'static str> {
catalogue().ids()
}
pub fn raise(id: &str, slots: &[(&str, &str)]) -> Refusal {
catalogue().raise(id, slots).unwrap_or_else(|| unknown(id))
}
pub fn unknown(id: &str) -> Refusal {
internal(id, &format!("RaiseMsg: unknown message id '{id}'"))
}
fn internal(id: &str, text: &str) -> Refusal {
Refusal {
id: id.to_string(),
number: 5,
source: "VLA-Messages".to_string(),
text: text.to_string(),
}
}
fn substitute_slots(template: &str, slots: &[(&str, &str)]) -> Result<String, String> {
let mut out = String::with_capacity(template.len());
let mut rest = template;
loop {
let Some(open) = rest.find('{') else {
out.push_str(rest);
return Ok(out);
};
let Some(close_rel) = rest[open..].find('}') else {
out.push_str(rest);
return Ok(out);
};
let close = open + close_rel;
out.push_str(&rest[..open]);
let name = &rest[open + 1..close];
match slots.iter().find(|(k, _)| *k == name) {
Some((_, v)) => out.push_str(v),
None => return Err(name.to_string()),
}
rest = &rest[close + 1..];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_language_s_catalogue_is_read_whole() {
assert!(count() >= 176, "{} entries read", count());
assert_eq!(ids()[0], "vla-source-not-found");
assert_eq!(
template("vla-unbalanced-parens").unwrap(),
"unbalanced parentheses: missing ')'{loc}"
);
assert!(template("view-sheet-unknown").is_some());
assert!(template("english-test-failed").is_none());
let r = raise(
"vla-interpreter-only-handler",
&[("name", "on:sheet-change")],
);
assert_eq!(r.number, -2147221504 + 7001);
assert_eq!(r.source, "VLA");
for id in ids() {
let family = id.split('-').next().unwrap();
assert!(
["vla", "interp", "lint", "view", "calc", "grid"].contains(&family),
"{id} is not the language's"
);
}
assert_eq!(
ids().iter().filter(|id| id.starts_with("grid-")).count(),
12
);
for id in ids().iter().filter(|id| id.starts_with("grid-")) {
let text = template(id).unwrap();
assert!(!text.contains("{line}"), "{id}: {text}");
}
for id in [
"view-projection-unknown",
"view-window-too-large",
"calc-construct-not-read",
] {
assert!(template(id).is_some(), "{id}");
}
}
#[test]
fn raise_fills_slots_as_raisemsg_does() {
let r = raise("vla-form-missing-element", &[("n", "3")]);
assert_eq!(r.id, "vla-form-missing-element");
assert_eq!(r.number, 5);
assert_eq!(r.source, "VLA");
assert_eq!(r.text, "form is missing required element 3");
let r = raise("vla-unbalanced-parens", &[("loc", "")]);
assert_eq!(r.text, "unbalanced parentheses: missing ')'");
let r = raise(
"vla-expected-list-at-position",
&[("n", "2"), ("value", "x")],
);
assert_eq!(r.text, "expected a list at position 2, got 'x'");
}
#[test]
fn the_two_internal_errors_are_refusals_too() {
let r = raise("no-such-id", &[]);
assert_eq!(r.number, 5);
assert_eq!(r.source, "VLA-Messages");
assert_eq!(r.text, "RaiseMsg: unknown message id 'no-such-id'");
let r = raise("vla-form-missing-element", &[]);
assert_eq!(r.text, "RaiseMsg: no value supplied for slot '{n}'");
assert!(catalogue().raise("no-such-id", &[]).is_none());
assert!(catalogue().raise("vla-form-missing-element", &[]).is_some());
}
#[test]
fn substitute_slots_copies_an_unclosed_brace() {
assert_eq!(
substitute_slots("a {x} b {", &[("x", "1")]).unwrap(),
"a 1 b {"
);
assert_eq!(substitute_slots("no slots", &[]).unwrap(), "no slots");
assert_eq!(
substitute_slots("{a}{b}", &[("a", "1"), ("b", "2")]).unwrap(),
"12"
);
}
}