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}