1use std::collections::BTreeMap;
5
6use super::{bool_of, embedded, str_of};
7use crate::{Error, Result, Spanned, Yaml, yaml};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TypeName {
12 String,
14 Integer,
16 Boolean,
18 List,
20 Map,
22 Object,
24 StringOrList,
26 StringOrMap,
28 Text,
31 TextOrRule,
33}
34
35impl TypeName {
36 pub fn parse(s: &str) -> Option<Self> {
38 Some(match s {
39 "string" => TypeName::String,
40 "integer" => TypeName::Integer,
41 "boolean" => TypeName::Boolean,
42 "list" => TypeName::List,
43 "map" => TypeName::Map,
44 "object" => TypeName::Object,
45 "string-or-list" => TypeName::StringOrList,
46 "string-or-map" => TypeName::StringOrMap,
47 "text" => TypeName::Text,
48 "text-or-rule" => TypeName::TextOrRule,
49 _ => return None,
50 })
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct KeyDef {
57 pub type_: TypeName,
59 pub of: Option<String>,
62 pub required: bool,
64 pub enum_: Vec<String>,
66 pub format: Option<String>,
69 pub requires_keys: Vec<String>,
71 pub requires_kind: Vec<String>,
73 pub doc: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Default)]
79pub struct RuleMap {
80 pub doc: String,
82 pub keys: Vec<(String, KeyDef)>,
84 pub exactly_one_of: Vec<String>,
86 pub at_least_one_of: Vec<String>,
88 pub common: bool,
90}
91
92impl RuleMap {
93 pub fn key(&self, key: &str) -> Option<&KeyDef> {
95 self.keys.iter().find(|(k, _)| k == key).map(|(_, d)| d)
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
102pub struct Grammar {
103 pub name: String,
106 pub version: String,
108 pub schema: RuleMap,
110 pub rules: BTreeMap<String, RuleMap>,
112 pub rule_common: RuleMap,
114 pub sections: BTreeMap<String, Spanned>,
116}
117impl Grammar {
120 pub fn rule(&self, name: &str) -> Option<&RuleMap> {
122 if name == "schema" {
123 return Some(&self.schema);
124 }
125 self.rules.get(name)
126 }
127
128 pub fn accepts_common(&self, name: &str) -> bool {
130 self.rule(name).is_some_and(|m| m.common)
131 }
132
133 pub fn section(&self, name: &str) -> Option<&Spanned> {
136 self.sections.get(name)
138 }
139
140 pub fn base(&mut self, text: &str) -> Result<()> {
152 self.transact(|g| g.lay_base(text))
154 }
155
156 fn transact(&mut self, lay: impl FnOnce(&mut Grammar) -> Result<()>) -> Result<()> {
160 let mut next = self.clone();
161 lay(&mut next)?;
162 *self = next;
163 Ok(())
164 }
165
166 fn lay_base(&mut self, text: &str) -> Result<()> {
167 let fail = |m: String| Error::Internal(format!("format grammar: {m}"));
168 let root = yaml::parse(text, 1).map_err(|e| fail(e.to_string()))?;
169 let Some(name) = root.get("grammar").and_then(|e| e.as_str()) else {
170 return Err(fail("no `grammar` name".into()));
171 };
172 let name = name.to_string();
173 match root.get("base").and_then(|e| e.as_str()) {
175 Some(base) if base == self.name => {}
176 other => {
177 return Err(fail(format!("`base` is {other:?}, not `{}`", self.name)));
178 }
179 }
180 for (key, value) in root.as_map().unwrap_or(&[]) {
181 match key.value.as_str() {
182 "grammar" | "base" | "doc" => {}
183 "version" => {
185 if let Some(v) = value.scalar_text() {
186 self.version = v;
187 }
188 }
189 "meta" => return Err(fail("`meta` is the core grammar's".into())),
190 "schema" => self.extend_schema(value, true).map_err(fail)?,
191 other => return Err(fail(format!("unknown section `{other}`"))),
193 }
194 }
195 self.check_references().map_err(fail)?;
196 self.name = name;
198 Ok(())
199 }
200
201 pub fn extend(&mut self, text: &str) -> Result<()> {
215 self.transact(|g| g.lay_extension(text))
217 }
218
219 fn lay_extension(&mut self, text: &str) -> Result<()> {
220 let fail = |m: String| Error::Internal(format!("schema extension: {m}"));
221 let root = yaml::parse(text, 1).map_err(|e| fail(e.to_string()))?;
222 if root.get("extension").and_then(|e| e.as_str()).is_none() {
223 return Err(fail("no `extension` name".into()));
224 }
225 match root.get("extends").and_then(|e| e.as_str()) {
226 Some(name) if name == self.name => {}
227 other => {
228 return Err(fail(format!("extends {other:?}, not `{}`", self.name)));
229 }
230 }
231 for (name, value) in root.as_map().unwrap_or(&[]) {
232 match name.value.as_str() {
233 "extension" | "extends" | "version" | "doc" => {}
234 "meta" => return Err(fail("`meta` is the core grammar's".into())),
235 "schema" => self.extend_schema(value, false).map_err(fail)?,
236 other if self.sections.contains_key(other) => {
238 return Err(fail(format!("section `{other}` twice")));
239 }
240 other => {
241 self.sections.insert(other.to_string(), value.clone());
242 }
243 }
244 }
245 self.check_references().map_err(fail)
246 }
247
248 fn check_references(&self) -> std::result::Result<(), String> {
252 let maps = [("schema", &self.schema), ("rule-common", &self.rule_common)]
253 .into_iter()
254 .chain(self.rules.iter().map(|(n, m)| (n.as_str(), m)));
255 for (name, map) in maps {
256 for (key, def) in &map.keys {
257 match def.of.as_deref() {
258 None | Some("string" | "integer" | "boolean" | "string-or-list") => {}
259 Some(of) if self.rule(of).is_some() => {}
260 Some(of) => {
261 return Err(format!(
262 "`{name}.{key}` holds `{of}`, which the grammar does not define"
263 ));
264 }
265 }
266 }
267 }
268 Ok(())
269 }
270
271 fn extend_schema(&mut self, node: &Spanned, format: bool) -> std::result::Result<(), String> {
275 for (name, value) in node.as_map().unwrap_or(&[]) {
276 match name.value.as_str() {
277 "keys" => extend_keys(&mut self.schema, value, "schema")?,
278 "doc" | "exactly-one-of" | "at-least-one-of" if format => {
279 add_to_map(&mut self.schema, name.value.as_str(), value);
280 }
281 "doc" | "exactly-one-of" | "at-least-one-of" | "rule-common" => {
282 return Err(format!("`schema.{}` is the core grammar's", name.value));
283 }
284 map => match self.rules.get_mut(map) {
285 Some(core) => {
286 for (k, v) in value.as_map().unwrap_or(&[]) {
287 match k.value.as_str() {
288 "keys" => extend_keys(core, v, map)?,
289 "doc" | "exactly-one-of" | "at-least-one-of" if format => {
290 add_to_map(core, k.value.as_str(), v);
291 }
292 other => {
293 return Err(format!("`{map}.{other}` is the core grammar's"));
294 }
295 }
296 }
297 }
298 None => {
299 let added = rule_map(value).map_err(|e| e.to_string())?;
300 self.rules.insert(map.to_string(), added);
301 }
302 },
303 }
304 }
305 Ok(())
306 }
307
308 pub fn key_def(&self, name: &str, key: &str) -> Option<&KeyDef> {
310 self.rule(name).and_then(|m| m.key(key)).or_else(|| {
311 if self.accepts_common(name) {
312 self.rule_common.key(key)
313 } else {
314 None
315 }
316 })
317 }
318}
319
320fn append_doc(doc: &mut String, more: &Spanned) {
322 let more = more.as_str().unwrap_or("").trim();
323 *doc = format!("{doc} {more}").trim().to_string();
324}
325
326fn add_to_map(map: &mut RuleMap, key: &str, value: &Spanned) {
328 match key {
329 "doc" => append_doc(&mut map.doc, value),
330 "exactly-one-of" => map.exactly_one_of.extend(string_list(Some(value))),
331 _ => map.at_least_one_of.extend(string_list(Some(value))),
332 }
333}
334
335fn extend_keys(map: &mut RuleMap, keys: &Spanned, name: &str) -> std::result::Result<(), String> {
338 for (key, def) in keys.as_map().unwrap_or(&[]) {
339 let Some(core) = map.keys.iter_mut().find(|(k, _)| *k == key.value) else {
340 map.keys
341 .push((key.value.clone(), key_def(def).map_err(|e| e.to_string())?));
342 continue;
343 };
344 let core = &mut core.1;
345 for (k, v) in def.as_map().unwrap_or(&[]) {
346 match k.value.as_str() {
347 "enum" => core.enum_.extend(
348 v.as_seq()
349 .unwrap_or(&[])
350 .iter()
351 .filter_map(|i| i.scalar_text()),
352 ),
353 "requires" if matches!(v.value, Yaml::Map(_)) => core.requires_kind.extend(
354 v.get("kind")
355 .and_then(|k| k.as_str_or_list())
356 .unwrap_or_default(),
357 ),
358 "doc" => append_doc(&mut core.doc, v),
359 other => {
360 return Err(format!(
361 "`{name}.{}` changes the core grammar's `{other}`",
362 key.value
363 ));
364 }
365 }
366 }
367 }
368 Ok(())
369}
370
371pub fn load_core_grammar() -> Result<Grammar> {
375 parse_grammar(embedded::grammar_text())
376}
377pub fn parse_grammar(text: &str) -> Result<Grammar> {
384 let root = yaml::parse(text, 1).map_err(|e| Error::Internal(format!("grammar: {e}")))?;
386 let name = root
387 .get("grammar")
388 .and_then(|v| v.as_str())
389 .ok_or_else(|| Error::Internal("grammar: no `grammar` name".into()))?
390 .to_string();
391 let version = root
392 .get("version")
393 .and_then(|v| v.scalar_text())
394 .ok_or_else(|| Error::Internal("grammar: no version".into()))?;
395 let schema_node = root
396 .get("schema")
397 .ok_or_else(|| Error::Internal("grammar: no schema section".into()))?;
398 let schema = rule_map(schema_node)?;
399 let mut rules = BTreeMap::new();
400 let mut rule_common = RuleMap::default();
401 for (key, value) in schema_node.as_map().unwrap_or(&[]) {
402 match key.value.as_str() {
403 "doc" | "keys" | "exactly-one-of" | "at-least-one-of" | "common" => {}
404 "rule-common" => rule_common = rule_map(value)?,
405 name => {
406 rules.insert(name.to_string(), rule_map(value)?);
407 }
408 }
409 }
410 let grammar = Grammar {
411 name,
412 version,
413 schema,
414 rules,
415 rule_common,
416 sections: BTreeMap::new(),
417 };
418 grammar
419 .check_references()
420 .map_err(|e| Error::Internal(format!("grammar: {e}")))?;
421 Ok(grammar)
422}
423
424fn rule_map(node: &Spanned) -> Result<RuleMap> {
425 let mut map = RuleMap {
426 doc: doc_of(node),
427 ..RuleMap::default()
428 };
429 if let Some(keys) = node.get("keys") {
430 for (name, def) in keys.as_map().unwrap_or(&[]) {
431 map.keys.push((name.value.clone(), key_def(def)?));
432 }
433 }
434 map.exactly_one_of = string_list(node.get("exactly-one-of"));
435 map.at_least_one_of = string_list(node.get("at-least-one-of"));
436 map.common = bool_of(node, "common");
437 Ok(map)
438}
439
440fn key_def(node: &Spanned) -> Result<KeyDef> {
441 let type_ = match node.get("type").and_then(|t| t.as_str()) {
442 Some(t) => TypeName::parse(t)
443 .ok_or_else(|| Error::Internal(format!("grammar: unknown type `{t}`")))?,
444 None => TypeName::String,
445 };
446 let mut requires_keys = Vec::new();
447 let mut requires_kind = Vec::new();
448 if let Some(r) = node.get("requires") {
449 match &r.value {
450 Yaml::Seq(_) => requires_keys = string_list(Some(r)),
451 Yaml::Map(_) => {
452 requires_kind = r
453 .get("kind")
454 .and_then(|k| k.as_str_or_list())
455 .unwrap_or_default()
456 }
457 _ => {}
458 }
459 }
460 Ok(KeyDef {
461 type_,
462 of: str_of(node, "of"),
463 required: bool_of(node, "required"),
464 enum_: node
465 .get("enum")
466 .and_then(|e| e.as_seq())
467 .map(|items| items.iter().filter_map(|i| i.scalar_text()).collect())
468 .unwrap_or_default(),
469 format: str_of(node, "format"),
470 requires_keys,
471 requires_kind,
472 doc: doc_of(node),
473 })
474}
475
476fn doc_of(node: &Spanned) -> String {
478 str_of(node, "doc").unwrap_or_default().trim().to_string()
479}
480
481fn string_list(node: Option<&Spanned>) -> Vec<String> {
482 node.and_then(|n| n.as_str_or_list()).unwrap_or_default()
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 const FORMAT: &str = "grammar: fmt\nbase: tramlines\nversion: '0.2'\ndoc: A format.\nschema:\n doc: More.\n at-least-one-of: [body]\n keys:\n body:\n type: list\n of: part\n part:\n common: true\n keys:\n text:\n type: string\n frontmatter-constraint:\n exactly-one-of: [const, enum]\n keys:\n format:\n enum: [email]\n";
490
491 #[test]
493 fn loads_the_core_grammar() {
494 let g = load_core_grammar().unwrap();
495 assert_eq!(g.name, "tramlines");
496 assert_eq!(g.version, "0.1");
497 assert!(g.schema.key("line-limit").unwrap().required);
498 assert!(g.schema.key("frontmatter").is_some());
499 assert!(g.schema.key("sections").is_none());
500 assert!(g.schema.at_least_one_of.is_empty());
501 assert!(g.rule("section").is_none());
502 assert!(g.rule("frontmatter-constraint").is_some());
503 assert!(g.rule_common.key("severity").is_some());
504 }
505
506 #[test]
509 fn a_format_grammar_adds_keys_rule_maps_and_constraint_groups() {
510 let mut g = load_core_grammar().unwrap();
511 g.base(FORMAT).unwrap();
512 assert_eq!(g.name, "fmt");
513 assert_eq!(g.version, "0.2");
514 assert!(g.schema.doc.ends_with("More."));
515 assert_eq!(g.schema.at_least_one_of, vec!["body"]);
516 assert!(g.schema.key("body").is_some());
517 assert!(g.accepts_common("part"));
518 let fm = g.rule("frontmatter-constraint").unwrap();
519 assert_eq!(fm.exactly_one_of, vec!["const", "enum"]);
520 assert!(
521 fm.key("format")
522 .unwrap()
523 .enum_
524 .contains(&"email".to_string())
525 );
526 }
527
528 #[test]
530 fn a_format_grammar_that_changes_the_core_is_refused() {
531 let base = |text: &str| load_core_grammar().unwrap().base(text);
532 assert!(base("base: tramlines\n").is_err());
533 assert!(base("grammar: fmt\nbase: other\n").is_err());
534 assert!(base("grammar: fmt\nbase: tramlines\nmeta: {}\n").is_err());
535 assert!(base("grammar: fmt\nbase: tramlines\nextra: {}\n").is_err());
536 assert!(
537 base("grammar: fmt\nbase: tramlines\nschema:\n keys:\n line-limit:\n type: string\n")
538 .is_err()
539 );
540 assert!(base("grammar: fmt\nbase: tramlines\nschema:\n rule-common: {}\n").is_err());
541 }
542
543 #[test]
548 fn a_host_extension_only_adds_to_its_format_grammar() {
549 let mut g = load_core_grammar().unwrap();
550 g.base(FORMAT).unwrap();
551 let mut h = g.clone();
552 h.extend("extension: host\nextends: fmt\nschema:\n keys:\n links:\n type: list\n part:\n keys:\n text:\n doc: Also this.\nnotes:\n a: 1\n")
553 .unwrap();
554 assert!(h.schema.key("links").is_some());
555 assert!(h.section("notes").is_some());
556 let refused = |text: &str| g.clone().extend(text);
557 assert!(refused("extends: fmt\n").is_err());
558 assert!(refused("extension: host\nextends: tramlines\n").is_err());
559 assert!(refused("extension: host\nextends: fmt\nmeta: {}\n").is_err());
560 assert!(
561 refused("extension: host\nextends: fmt\nschema:\n at-least-one-of: [links]\n")
562 .is_err()
563 );
564 assert!(
565 refused(
566 "extension: host\nextends: fmt\nschema:\n part:\n exactly-one-of: [text]\n"
567 )
568 .is_err()
569 );
570 assert!(refused("extension: host\nextends: fmt\nschema:\n part:\n keys:\n text:\n type: integer\n").is_err());
571 assert!(refused("extension: host\nextends: fmt\nnotes: {}\nnotes: {}\n").is_err());
572 }
573
574 #[test]
575 fn a_grammar_whose_of_names_nothing_is_refused() {
576 let mut g = load_core_grammar().unwrap();
577 let format = "grammar: fmt\nbase: tramlines\nversion: '0.2'\nschema:\n keys:\n body:\n type: list\n of: nothing\n";
578 let before = g.clone();
579 let e = g.base(format).unwrap_err().to_string();
580 assert!(e.contains("`schema.body` holds `nothing`"), "{e}");
581 assert_eq!(g, before);
583 let core = "grammar: x\nversion: '1'\nschema:\n keys:\n a:\n type: list\n of: nothing\n";
584 assert!(parse_grammar(core).is_err());
585 }
586
587 #[test]
588 fn refuses_a_broken_grammar() {
589 assert!(parse_grammar("grammar: x\n").is_err());
590 assert!(parse_grammar("version: '1'\nschema: {}\n").is_err());
591 assert!(
592 parse_grammar("grammar: x\nversion: '1'\nschema:\n keys:\n a:\n type: nope\n")
593 .is_err()
594 );
595 assert!(parse_grammar("a: &x 1\n").is_err());
596 let g = parse_grammar("grammar: x\nversion: '1'\nschema:\n keys:\n a: {}\n").unwrap();
597 assert_eq!(g.schema.key("a").unwrap().type_, TypeName::String);
598 assert!(TypeName::parse("nope").is_none());
599 }
600}