Skip to main content

tramlines_core/grammar/
model.rs

1//! The schema keys every format shares, lowered: the frontmatter, the
2//! lifecycle, the formats, the examples and the bans of a schema, and the
3//! keys every rule map carries.
4
5use std::collections::BTreeMap;
6
7use super::{FormatRegistry, IdTemplate, Shape};
8use crate::{Severity, Spanned};
9
10/// The scope of a captured key.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Scope {
13    /// Unique within the document.
14    Document,
15    /// Unique within a scope the host names and checks, as `knowledge`.
16    Host(String),
17}
18
19impl Scope {
20    /// The scope a `unique` value names; absent means the document.
21    pub fn parse(s: Option<&str>) -> Scope {
22        match s {
23            None | Some("document") => Scope::Document,
24            Some(host) => Scope::Host(host.to_string()),
25        }
26    }
27}
28
29/// Lifecycle dimension to admitted states (grammar `only`).
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct Only(pub BTreeMap<String, Vec<String>>);
32
33impl Only {
34    /// Whether the rule applies to a document in `states`: for every
35    /// dimension named, the document's states intersect the admitted ones.
36    pub fn admits(&self, states: &States) -> bool {
37        self.0.iter().all(|(dim, admitted)| {
38            states
39                .0
40                .get(dim)
41                .is_some_and(|have| have.iter().any(|s| admitted.contains(s)))
42        })
43    }
44
45    /// Whether some document's states admit both this rule and `other`:
46    /// each dimension both name admits a state in common.
47    pub fn overlaps(&self, other: &Only) -> bool {
48        self.0.iter().all(|(dim, admitted)| {
49            other
50                .0
51                .get(dim)
52                .is_none_or(|theirs| theirs.iter().any(|s| admitted.contains(s)))
53        })
54    }
55
56    /// Whether the rule is gated at all.
57    pub fn is_gated(&self) -> bool {
58        !self.0.is_empty()
59    }
60
61    /// `k=v,v2 k2=v`, as an `only` marker writes it.
62    pub fn render(&self) -> String {
63        self.0
64            .iter()
65            .map(|(k, v)| format!("{k}={}", v.join(",")))
66            .collect::<Vec<_>>()
67            .join(" ")
68    }
69}
70
71/// A document's lifecycle states: one per dimension, or a set for an
72/// example.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct States(pub BTreeMap<String, Vec<String>>);
75
76/// Grammar `rule-common` plus `only`.
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct RuleCommon {
79    /// Error or warning.
80    pub severity: Severity,
81    /// When the rule applies.
82    pub only: Only,
83    /// The rule's line in the schema.
84    pub line: usize,
85}
86
87/// A grammar `text` value: a literal, or a frontmatter key whose value
88/// stands in for it. Frontmatter is the authority; a repair rewrites the
89/// body.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum Text {
92    /// The string as written.
93    Literal(String),
94    /// `{ from: <key> }`.
95    From(String),
96}
97
98/// Grammar `key-derivation`.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct KeyDerivation {
101    /// Where the allowed prefixes come from.
102    pub source: DerivationSource,
103    /// The literal ending the prefix.
104    pub separator: String,
105}
106
107/// Where a `key-derivation` takes its prefixes (grammar exactly-one-of).
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum DerivationSource {
110    /// `rule`: the keys that rule captured.
111    Rule(String),
112    /// `from`: the value of a frontmatter key, the one prefix.
113    From(String),
114}
115
116impl KeyDerivation {
117    /// The source as a finding names it: `rule \`x\`` or `the frontmatter \`x\``.
118    pub fn source_label(&self) -> String {
119        match &self.source {
120            DerivationSource::Rule(r) => format!("rule `{r}`"),
121            DerivationSource::From(k) => format!("the frontmatter `{k}`"),
122        }
123    }
124}
125
126/// `format` of a frontmatter constraint.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum FmFormat {
129    /// An absolute URL.
130    Url,
131    /// A slug: lower-case letters and digits joined by single hyphens.
132    Slug,
133    /// A format the host names and checks, as `document-id`.
134    Host(String),
135}
136
137impl FmFormat {
138    /// The format's name in the grammar.
139    pub fn name(&self) -> &str {
140        match self {
141            FmFormat::Url => "url",
142            FmFormat::Slug => "slug",
143            FmFormat::Host(name) => name,
144        }
145    }
146}
147
148/// Grammar `frontmatter-constraint`.
149#[derive(Debug, Clone)]
150pub struct FrontmatterConstraint {
151    /// The common keys.
152    pub common: RuleCommon,
153    /// `const`.
154    pub const_: Option<String>,
155    /// `template` or `pattern`: the value's shape.
156    pub shape: Option<Shape>,
157    /// `enum`.
158    pub enum_: Option<Vec<String>>,
159    /// `format`.
160    pub format: Option<FmFormat>,
161    /// The id a new document is filled with, from the `id` key's template.
162    pub id_template: Option<IdTemplate>,
163    /// `list`.
164    pub list: bool,
165    /// `min`.
166    pub min: Option<usize>,
167    /// `max`.
168    pub max: Option<usize>,
169    /// `optional`.
170    pub optional: bool,
171    /// `example`: a value of the kind this key demands.
172    pub example: Option<String>,
173    /// `guidance`: what the example cannot say, written on a comment line before the key.
174    pub guidance: Option<String>,
175}
176
177/// Grammar `lifecycle-dimension`.
178#[derive(Debug, Clone, PartialEq)]
179pub struct LifecycleDimension {
180    /// `key`.
181    pub key: String,
182    /// `states`.
183    pub states: Vec<String>,
184    /// `guidance`: what the states mean, written on a comment line before the key.
185    pub guidance: Option<String>,
186    /// The line in the schema.
187    pub line: usize,
188    /// The keys a host's extension adds, as written: the host lowers what
189    /// it adds.
190    pub extensions: BTreeMap<String, Spanned>,
191}
192
193/// Grammar `example`.
194#[derive(Debug, Clone)]
195pub struct Example {
196    /// `document`.
197    pub document: String,
198    /// The line in the schema.
199    pub line: usize,
200}
201
202// sokf:begin core-schema
203/// The part of a lowered schema that every format shares: grammar
204/// `schema`, the keys of the core grammar.
205#[derive(Debug, Clone)]
206pub struct CoreSchema {
207    /// `line-limit`; zero or less declares no limit.
208    pub line_limit: i64,
209    /// `lifecycle`.
210    pub lifecycle: Vec<LifecycleDimension>,
211    /// `frontmatter`, in schema order.
212    pub frontmatter: Vec<(String, FrontmatterConstraint)>,
213    /// `frontmatter-open`.
214    pub frontmatter_open: bool,
215    /// `prohibited-patterns`: what no line of the body may match.
216    pub prohibited_patterns: Vec<super::Prohibition>,
217    /// The registry as this schema sees it.
218    pub formats: FormatRegistry,
219    /// `guidance`: what the schema says that no example can.
220    pub guidance: Option<String>,
221    /// `examples`.
222    pub examples: Vec<Example>,
223    /// The line of the schema where it is written.
224    pub line: usize,
225    /// The line of each key of the schema, by the key.
226    pub key_lines: BTreeMap<String, usize>,
227}
228// sokf:end core-schema
229
230impl CoreSchema {
231    /// A schema that demands nothing.
232    pub fn demand_nothing() -> CoreSchema {
233        CoreSchema {
234            line_limit: 0,
235            lifecycle: vec![],
236            frontmatter: vec![],
237            frontmatter_open: true,
238            prohibited_patterns: vec![],
239            formats: FormatRegistry::default(),
240            guidance: None,
241            examples: vec![],
242            line: 0,
243            key_lines: BTreeMap::new(),
244        }
245    }
246
247    /// A document's lifecycle states, one per dimension its frontmatter
248    /// carries.
249    pub fn states(&self, frontmatter: Option<&Spanned>) -> States {
250        let mut states = States::default();
251        for dim in &self.lifecycle {
252            if let Some(value) = frontmatter
253                .and_then(|f| f.get(&dim.key))
254                .and_then(|v| v.scalar_text())
255            {
256                states.0.insert(dim.key.clone(), vec![value]);
257            }
258        }
259        states
260    }
261
262    /// The `type` const, where the `type` key declares one.
263    pub fn type_const(&self) -> Option<&str> {
264        self.constraint("type")?.const_.as_deref()
265    }
266
267    /// The constraint the schema declares on the frontmatter `key`.
268    // sokf:begin core-schema
269    pub fn constraint(&self, key: &str) -> Option<&FrontmatterConstraint> {
270        // sokf:end core-schema
271        // sokf-implements: CORE-6_AC-9
272        self.frontmatter
273            .iter()
274            .find(|(k, _)| k == key)
275            .map(|(_, c)| c)
276    }
277
278    /// The id template, if `frontmatter.id.template` is declared.
279    pub fn id_template(&self) -> Option<&IdTemplate> {
280        self.constraint("id")?.id_template.as_ref()
281    }
282
283    /// The lifecycle dimension with `key`.
284    pub fn dimension(&self, key: &str) -> Option<&LifecycleDimension> {
285        self.lifecycle.iter().find(|d| d.key == key)
286    }
287}