Skip to main content

tramlines_core/grammar/
meta.rs

1//! The parsed grammar: its `meta` vocabulary and the `schema` rule maps,
2//! with what a format grammar and a host's extension add.
3
4use std::collections::BTreeMap;
5
6use super::{bool_of, embedded, str_of};
7use crate::{Error, Result, Spanned, Yaml, yaml};
8
9/// The `meta.types` of the grammar.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TypeName {
12    /// A string.
13    String,
14    /// An integer.
15    Integer,
16    /// A boolean.
17    Boolean,
18    /// A list of `of`.
19    List,
20    /// A keyed map of `of`.
21    Map,
22    /// One instance of `of`.
23    Object,
24    /// A string or a list of strings.
25    StringOrList,
26    /// A string or a map.
27    StringOrMap,
28    /// A string, or `{ from: <frontmatter key> }` standing for the key's
29    /// value.
30    Text,
31    /// A `text`, or one instance of the rule map `of`.
32    TextOrRule,
33}
34
35impl TypeName {
36    /// The type the grammar names `s`.
37    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/// One key of a rule map, as `meta.keys` describes it.
55#[derive(Debug, Clone, PartialEq)]
56pub struct KeyDef {
57    /// The value's type.
58    pub type_: TypeName,
59    /// The item, value or instance shape: a type, a scalar format or a rule
60    /// map name.
61    pub of: Option<String>,
62    /// Whether a schema must carry the key.
63    pub required: bool,
64    /// The values the key admits, as text.
65    pub enum_: Vec<String>,
66    /// A scalar format or a reference: `regex`, `slug`, `rule-id`,
67    /// `format-name`.
68    pub format: Option<String>,
69    /// Keys that must be present beside this one.
70    pub requires_keys: Vec<String>,
71    /// Block kinds the enclosing block must be one of.
72    pub requires_kind: Vec<String>,
73    /// What the key means.
74    pub doc: String,
75}
76
77/// One rule map under `schema`, or `schema` itself.
78#[derive(Debug, Clone, PartialEq, Default)]
79pub struct RuleMap {
80    /// What the map is.
81    pub doc: String,
82    /// The keys, in grammar order.
83    pub keys: Vec<(String, KeyDef)>,
84    /// Exactly one of these keys must be present.
85    pub exactly_one_of: Vec<String>,
86    /// At least one of these keys must be present.
87    pub at_least_one_of: Vec<String>,
88    /// Whether the map accepts the `rule-common` keys.
89    pub common: bool,
90}
91
92impl RuleMap {
93    /// The definition of `key`.
94    pub fn key(&self, key: &str) -> Option<&KeyDef> {
95        self.keys.iter().find(|(k, _)| k == key).map(|(_, d)| d)
96    }
97}
98
99// sokf:begin grammar
100/// The parsed grammar file.
101#[derive(Debug, Clone, PartialEq)]
102pub struct Grammar {
103    /// The name of the grammar: `tramlines`, then the name of the format
104    /// grammar laid on it.
105    pub name: String,
106    /// The grammar version.
107    pub version: String,
108    /// The `schema` vocabulary.
109    pub schema: RuleMap,
110    /// The rule maps under `schema`, by name.
111    pub rules: BTreeMap<String, RuleMap>,
112    /// The `rule-common` keys.
113    pub rule_common: RuleMap,
114    /// The sections an extension adds beside `schema`, by name, as written.
115    pub sections: BTreeMap<String, Spanned>,
116}
117// sokf:end grammar
118
119impl Grammar {
120    /// The rule map named `name`.
121    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    /// Whether `name` is a rule map that accepts the `rule-common` keys.
129    pub fn accepts_common(&self, name: &str) -> bool {
130        self.rule(name).is_some_and(|m| m.common)
131    }
132
133    /// A section an extension adds, by name.
134    // sokf:begin grammar-extend
135    pub fn section(&self, name: &str) -> Option<&Spanned> {
136        // sokf:end grammar-extend
137        self.sections.get(name)
138    }
139
140    /// Lay a format grammar on this one, the core grammar: it names
141    /// itself with `grammar` and its base with `base`, and adds keys, rule
142    /// maps, values of an enum, block kinds a key requires, text of a
143    /// `doc`, and the constraint groups of any rule map. A format grammar
144    /// that changes what the core grammar says is a defect of the build.
145    // sokf-implements: TL_2
146    // sokf-implements: TL_3
147    // sokf-implements: TL_4
148    // sokf-implements: CORE-2_AC-1
149    // sokf-implements: CORE-2_AC-2
150    // sokf:begin grammar-base
151    pub fn base(&mut self, text: &str) -> Result<()> {
152        // sokf:end grammar-base
153        self.transact(|g| g.lay_base(text))
154    }
155
156    /// Lay a layer with `lay` on a copy of the grammar. A layer that fails
157    /// leaves the grammar as it was, so no half-laid layer reaches the
158    /// validator.
159    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        // sokf-implements: CORE-2_AC-3
174        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                // sokf-implements: CORE-2_AC-6
184                "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                // sokf-implements: CORE-2_AC-4
192                other => return Err(fail(format!("unknown section `{other}`"))),
193            }
194        }
195        self.check_references().map_err(fail)?;
196        // sokf-implements: CORE-2_AC-5
197        self.name = name;
198        Ok(())
199    }
200
201    /// Add a host's extension, which names itself with `extension` and
202    /// names the format grammar it extends with `extends`: keys, rule maps
203    /// and sections of its own; on a key of the grammar, more `enum`
204    /// values, more block kinds it `requires`, and more `doc`. An extension
205    /// that changes what the grammar says is a defect of the build.
206    // sokf-implements: TL_5
207    // sokf-implements: TL_6
208    // sokf-implements: TL_7
209    // sokf-implements: TL_8
210    // sokf-implements: CORE-3_AC-1
211    // sokf-implements: CORE-3_AC-2
212    // sokf-implements: CORE-3_AC-5
213    // sokf:begin grammar-extend
214    pub fn extend(&mut self, text: &str) -> Result<()> {
215        // sokf:end grammar-extend
216        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                // sokf-implements: CORE-3_AC-4
237                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    /// Refuse an `of` that names neither a scalar nor a rule map, so that
249    /// the validator, which follows each one, never meets one that names
250    /// nothing: the fault is the grammar's, and no schema can mend it.
251    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    /// Add `node`, a `schema` section, to this grammar's. A format grammar
272    /// may also add text to a `doc` and constraint groups to a rule map; a
273    /// host extension may not.
274    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    /// The definition of `key` in rule map `name`, common keys included.
309    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
320/// Append the text of a layer's `doc` to `doc`.
321fn 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
326/// Add a format grammar's `doc` text or constraint group to `map`.
327fn 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
335/// Add an extension's `keys` to a core rule map: a new key whole; on a
336/// core key, only more `enum` values, more required kinds and more `doc`.
337fn 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
371// sokf-implements: CORE-1_AC-1
372// sokf:begin grammar-load
373/// Parse the embedded core grammar, onto which a format grammar is merged.
374pub fn load_core_grammar() -> Result<Grammar> {
375    parse_grammar(embedded::grammar_text())
376}
377// sokf:end grammar-load
378
379/// Parse a core grammar file.
380// sokf-implements: CORE-1_AC-2
381// sokf-implements: CORE-1_AC-3
382// sokf:begin grammar-load
383pub fn parse_grammar(text: &str) -> Result<Grammar> {
384    // sokf:end grammar-load
385    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
476/// The `doc` of a grammar node, trimmed, or empty.
477fn 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    // sokf-tests: TL_1
492    #[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    // sokf-tests: TL_2
507    // sokf-tests: TL_3
508    #[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    // sokf-tests: TL_4
529    #[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    // sokf-tests: TL_5
544    // sokf-tests: TL_6
545    // sokf-tests: TL_7
546    // sokf-tests: TL_8
547    #[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        // A layer that fails leaves the grammar as it was.
582        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}