Skip to main content

tramlines_core/grammar/lower/
shapes.rs

1//! The shapes a schema declares: a rule's `text`, `template` or
2//! `pattern`, the key it names, and a frontmatter value's template, the
3//! `id`'s the one a new document is filled with.
4
5use super::{Lowerer, bool_of, str_of, usize_of};
6use crate::{
7    Spanned,
8    grammar::{FmFormat, Format, FrontmatterConstraint, IdTemplate, Shape, shape_of},
9};
10
11impl Lowerer<'_> {
12    /// The shape `node` declares, a failure reported on the schema.
13    pub fn shape(&mut self, node: &Spanned) -> Option<Shape> {
14        match shape_of(node, &self.types, self.regexes) {
15            Ok(s) => s,
16            Err(e) => {
17                self.fail_with(node.line, e);
18                None
19            }
20        }
21    }
22
23    /// A `key` names a hole or a named group of `shape`, or of every shape
24    /// of the format the rule binds.
25    // sokf-implements: TL_31
26    pub fn check_key(
27        &mut self,
28        line: usize,
29        key: &str,
30        shape: Option<&Shape>,
31        format: Option<&Format>,
32    ) {
33        let shapes: Vec<&Shape> = match (shape, format) {
34            (Some(s), _) => vec![s],
35            (None, Some(f)) => f.shapes().map(|(_, s)| s).collect(),
36            (None, None) => vec![],
37        };
38        if shapes.is_empty() || shapes.iter().any(|s| !s.has_group(key)) {
39            self.fail(
40                line,
41                "TL_31",
42                format!("`key` `{key}` names no hole or named group of the rule's shape"),
43            );
44        }
45    }
46
47    /// Grammar `frontmatter-constraint`, for the key `key`.
48    pub fn frontmatter_constraint(&mut self, node: &Spanned, key: &str) -> FrontmatterConstraint {
49        // The scaffold writes the id the template generates, so the `id`
50        // template needs no example to speak for it.
51        if key != "id" {
52            self.explained(node, &["pattern", "template"], false);
53        }
54        self.one_voice(node);
55        let shape = self.shape(node);
56        // `id` is a key with a template like any other; its template is
57        // also the one a new document is filled with, so it holds only the holes the
58        // tool generates, and nothing narrows it beside the template.
59        let width = usize_of(node, "seq-width");
60        let mut id_template = None;
61        match (&shape, key) {
62            (Some(Shape::Template(t)), "id") => {
63                for other in ["const", "enum", "format", "list", "min", "max"] {
64                    if let Some(o) = node.get(other) {
65                        // sokf-implements: TL_115
66                        self.fail(
67                            o.line,
68                            "TL_115",
69                            format!(
70                                "`{other}` beside the `id` template: `id` takes `template` alone"
71                            ),
72                        );
73                    }
74                }
75                match IdTemplate::from_template(t, width.unwrap_or(3), &self.lifecycle) {
76                    Ok(parsed) => {
77                        if width.is_some() && parsed.seq_width().is_none() {
78                            // sokf-implements: TL_116
79                            self.fail(
80                                node.line,
81                                "TL_116",
82                                "`seq-width` beside a template with no `seq` hole",
83                            );
84                        }
85                        id_template = Some(parsed);
86                    }
87                    Err(e) => self.fail(node.line, e.criterion, e.message),
88                }
89            }
90            // A fixed id is the one a new document takes, as a template with
91            // no hole would be.
92            (None, "id") if node.get("const").is_some() => {
93                match IdTemplate::fixed(&str_of(node, "const").unwrap_or_default()) {
94                    Ok(fixed) => id_template = Some(fixed),
95                    Err(e) => self.fail(node.line, e.criterion, e.message),
96                }
97            }
98            _ => {
99                if width.is_some() {
100                    self.fail(
101                        node.line,
102                        "TL_116",
103                        "`seq-width` applies to the `id` template alone",
104                    );
105                }
106            }
107        }
108        if let Some(min) = usize_of(node, "min") {
109            self.check_bounds(node.line, min, usize_of(node, "max"));
110        }
111        FrontmatterConstraint {
112            common: self.common(node),
113            const_: node.get("const").and_then(|c| c.scalar_text()),
114            shape,
115            enum_: node
116                .get("enum")
117                .and_then(|e| e.as_seq())
118                .map(|items| items.iter().filter_map(|i| i.scalar_text()).collect()),
119            // A format the core does not know is the host's: the merged
120            // grammar admitted it.
121            format: str_of(node, "format").map(|f| match f.as_str() {
122                "url" => FmFormat::Url,
123                "slug" => FmFormat::Slug,
124                _ => FmFormat::Host(f),
125            }),
126            id_template,
127            list: bool_of(node, "list"),
128            min: usize_of(node, "min"),
129            max: usize_of(node, "max"),
130            optional: bool_of(node, "optional"),
131            example: str_of(node, "example"),
132            guidance: str_of(node, "guidance"),
133        }
134    }
135}