okf_core/frontmatter.rs
1//! Typed, order-preserving access to a concept's YAML frontmatter.
2//!
3//! OKF frontmatter is an open mapping: a few well-known keys (§4.1 of the
4//! [spec]) plus arbitrary producer-defined extensions that consumers MUST
5//! preserve when round-tripping. [`Frontmatter`] therefore stores the full
6//! [`Mapping`] verbatim and layers typed accessors on top, rather than
7//! deserializing into a fixed struct that would drop unknown keys.
8//!
9//! v0.2 adds four families of well-known keys on top of the v0.1 core, all of
10//! them optional:
11//!
12//! | Family | Keys | Section |
13//! |---------------------------|----------------------------------------------------------|---------|
14//! | Core | `type`, `title`, `description`, `resource`, `tags` | §4.1 |
15//! | Provenance | `sources`, `usage_window` | §5.1 |
16//! | Trust | `generated`, `verified` | §5.2 |
17//! | Lifecycle | `status`, `stale_after` | §5.4/5 |
18//! | Computation | `runtime`, `parameters`, `computation`, `executor`, `attester` | §10.2 |
19//!
20//! Absence is meaningful but never fatal: [`Frontmatter::status`] defaults to
21//! `stable`, [`Frontmatter::trust_tier`] to `unverified`, and a concept
22//! carrying nothing but `type` is fully conformant (§11).
23//!
24//! [spec]: https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md
25
26use crate::computation::{ATTESTED_COMPUTATION_TYPE, Attester, Executor, Parameter};
27use crate::date::{Date, DateTime, DateTimeField};
28use crate::provenance::{Source, UsageWindow};
29use crate::trust::{self, Generated, Status, TrustTier, Verification};
30use crate::yaml::{Mapping, Value};
31use std::borrow::Cow;
32
33/// The only frontmatter key OKF always requires (§4.1): a concept carrying
34/// nothing but `type` is fully conformant (§11).
35///
36/// This is what [`Document::validate`](crate::Document::validate) enforces, and
37/// it matches the reference implementation's `REQUIRED_FRONTMATTER_KEYS`. v0.1
38/// required four keys; v0.2 narrowed the requirement to this one and demoted
39/// the rest to recommendations ([`RECOMMENDED_FRONTMATTER_KEYS`]).
40pub const REQUIRED_FRONTMATTER_KEYS: [&str; 1] = ["type"];
41
42/// Keys a producer should fill in before publishing, in the order
43/// [`Document::missing_recommended`](crate::Document::missing_recommended)
44/// reports them.
45///
46/// `title` and `description` are §4.1 recommendations; `generated` is §5.2's
47/// record of how the content was produced. §4.1 also recommends `resource` and
48/// `tags`, which are deliberately left out here: `resource` is "absent for
49/// concepts that describe abstract ideas rather than physical resources", so
50/// flagging either would be noise rather than guidance.
51///
52/// Leaving any of these unset is never a conformance failure (§11).
53pub const RECOMMENDED_FRONTMATTER_KEYS: [&str; 3] = ["title", "description", "generated"];
54
55/// Keys v0.2 retired but consumers may still encounter in v0.1 documents
56/// (§13.1). `timestamp` is superseded by `generated.at`.
57pub const LEGACY_FRONTMATTER_KEYS: [&str; 1] = ["timestamp"];
58
59/// Every frontmatter key the specification gives a meaning to, across all
60/// families. Anything else is a producer extension (§4.1).
61pub const KNOWN_FRONTMATTER_KEYS: [&str; 17] = [
62 // Core (§4.1).
63 "type",
64 "title",
65 "description",
66 "resource",
67 "tags",
68 // Provenance (§5.1).
69 "sources",
70 "usage_window",
71 // Trust (§5.2).
72 "generated",
73 "verified",
74 // Lifecycle (§5.4, §5.5).
75 "status",
76 "stale_after",
77 // Attested Computation (§10.2).
78 "runtime",
79 "parameters",
80 "computation",
81 "executor",
82 "attester",
83 // Legacy (§13.1).
84 "timestamp",
85];
86
87/// The key order the reference implementation writes documents in (its
88/// `_PREFERRED_KEY_ORDER`): identity first, then lifecycle, trust, and
89/// provenance.
90///
91/// Presentational only. §4.1 gives frontmatter no required key order, and a
92/// consumer must not depend on one; see [`Frontmatter::reorder_preferred`].
93pub const PREFERRED_KEY_ORDER: [&str; 11] = [
94 "type",
95 "resource",
96 "title",
97 "description",
98 "tags",
99 "status",
100 "generated",
101 "verified",
102 "stale_after",
103 "sources",
104 "usage_window",
105];
106
107/// A concept's frontmatter: an ordered key/value mapping with typed accessors
108/// for the well-known OKF fields.
109#[derive(Clone, Debug, Default, PartialEq)]
110pub struct Frontmatter {
111 map: Mapping,
112}
113
114impl Frontmatter {
115 /// Creates an empty frontmatter block.
116 #[must_use]
117 pub const fn new() -> Self {
118 Self {
119 map: Mapping::new(),
120 }
121 }
122
123 /// Wraps an existing mapping.
124 #[must_use]
125 pub const fn from_mapping(map: Mapping) -> Self {
126 Self { map }
127 }
128
129 /// Borrows the underlying ordered mapping.
130 #[must_use]
131 pub const fn as_mapping(&self) -> &Mapping {
132 &self.map
133 }
134
135 /// Mutably borrows the underlying ordered mapping.
136 pub const fn as_mapping_mut(&mut self) -> &mut Mapping {
137 &mut self.map
138 }
139
140 /// Consumes the wrapper, returning the underlying mapping.
141 #[must_use]
142 pub fn into_mapping(self) -> Mapping {
143 self.map
144 }
145
146 /// `true` if there are no keys.
147 #[must_use]
148 pub const fn is_empty(&self) -> bool {
149 self.map.is_empty()
150 }
151
152 /// Raw value for an arbitrary key (including producer extensions).
153 #[must_use]
154 pub fn get(&self, key: &str) -> Option<&Value> {
155 self.map.get(key)
156 }
157
158 /// Sets a raw value for a key, preserving position if it already exists.
159 pub fn set(&mut self, key: impl Into<String>, value: Value) {
160 self.map.insert(key, value);
161 }
162
163 /// Reorders the keys into [`PREFERRED_KEY_ORDER`], leaving every other key
164 /// after them in its current relative order.
165 ///
166 /// A port of the reference implementation's `_reorder_frontmatter`, which it
167 /// applies whenever it writes a concept document; call this before
168 /// [`Document::serialize`](crate::Document::serialize) to produce
169 /// frontmatter laid out the same way. No key is added, dropped, or
170 /// rewritten, so only the serialized order changes.
171 pub fn reorder_preferred(&mut self) {
172 let mut ordered = Mapping::new();
173 for key in PREFERRED_KEY_ORDER {
174 if let Some(value) = self.map.get(key) {
175 ordered.insert(key, value.clone());
176 }
177 }
178 for (key, value) in self.map.iter() {
179 let already_placed = key
180 .as_str()
181 .is_some_and(|k| PREFERRED_KEY_ORDER.contains(&k));
182 if !already_placed {
183 ordered.push_raw(key.clone(), value.clone());
184 }
185 }
186 self.map = ordered;
187 }
188
189 /// The **required** `type` field (§4.1). `None` if absent or not a scalar.
190 ///
191 /// Non-string scalars (`type: 42`) are coerced to their display form, the
192 /// way the reference's `str(fm.get("type"))` does, rather than read as
193 /// `None`: the spec calls `type` "a short string", so a non-string value
194 /// is a producer deviation, but a consumer still gets *something* to
195 /// route on rather than treating the concept as typeless.
196 #[must_use]
197 pub fn type_(&self) -> Option<Cow<'_, str>> {
198 self.display_str("type")
199 }
200
201 /// The optional `title` field.
202 #[must_use]
203 pub fn title(&self) -> Option<Cow<'_, str>> {
204 self.display_str("title")
205 }
206
207 /// The optional one-line `description`.
208 #[must_use]
209 pub fn description(&self) -> Option<Cow<'_, str>> {
210 self.display_str("description")
211 }
212
213 /// The optional `resource` URI for the underlying asset.
214 #[must_use]
215 pub fn resource(&self) -> Option<Cow<'_, str>> {
216 self.display_str("resource")
217 }
218
219 /// The optional `tags` list. Non-string elements are coerced to their
220 /// display form; a non-sequence `tags` value yields an empty vector.
221 pub fn tags(&self) -> Vec<String> {
222 match self.map.get("tags") {
223 Some(Value::Sequence(items)) => {
224 items.iter().filter_map(Value::as_display_string).collect()
225 }
226 _ => Vec::new(),
227 }
228 }
229
230 /// The `sources` entries: the materials this concept derives from.
231 pub fn sources(&self) -> Vec<Source> {
232 self.map
233 .get("sources")
234 .map(Source::list_from_value)
235 .unwrap_or_default()
236 }
237
238 /// The shared `usage_window` that frames every `sources[].usage_count`.
239 pub fn usage_window(&self) -> Option<UsageWindow> {
240 self.map
241 .get("usage_window")
242 .and_then(UsageWindow::from_value)
243 }
244
245 /// The `generated` block: how the current content was produced.
246 pub fn generated(&self) -> Option<Generated> {
247 self.map.get("generated").and_then(Generated::from_value)
248 }
249
250 /// The `verified` events: who or what has confirmed this content.
251 ///
252 /// A bare `{ by, at }` mapping is returned as a one-element list, as §5.2
253 /// requires.
254 pub fn verified(&self) -> Vec<Verification> {
255 self.map
256 .get("verified")
257 .map(Verification::list_from_value)
258 .unwrap_or_default()
259 }
260
261 /// The verification with the latest parseable `at` (§5.2).
262 #[must_use]
263 pub fn latest_verification(&self) -> Option<Verification> {
264 let events = self.verified();
265 trust::latest_verification(&events).cloned()
266 }
267
268 /// The trust tier derived from `verified` (§5.3).
269 #[must_use]
270 pub fn trust_tier(&self) -> TrustTier {
271 TrustTier::derive(&self.verified())
272 }
273
274 /// When the content last meaningfully changed: `generated.at` (§5.2),
275 /// falling back to a legacy v0.1 `timestamp` when `generated` is absent, as
276 /// §13.1 permits.
277 #[must_use]
278 pub fn content_changed_at(&self) -> Option<DateTimeField> {
279 self.generated()
280 .and_then(|g| g.at)
281 .or_else(|| self.timestamp().map(|s| DateTimeField::new(s.into_owned())))
282 }
283
284 /// The legacy v0.1 `timestamp` field, superseded by `generated.at` (§13.1).
285 ///
286 /// Prefer [`Frontmatter::content_changed_at`], which reads `generated.at`
287 /// first and falls back to this.
288 #[must_use]
289 pub fn timestamp(&self) -> Option<Cow<'_, str>> {
290 self.display_str("timestamp")
291 }
292
293 /// The lifecycle `status`. An absent key is [`Status::Stable`] (§5.4).
294 #[must_use]
295 pub fn status(&self) -> Status {
296 Status::parse(self.display_str("status").as_deref())
297 }
298
299 /// The `stale_after` timestamp, on and after which the content is stale (§5.5).
300 #[must_use]
301 pub fn stale_after(&self) -> Option<DateTimeField> {
302 self.display_str("stale_after")
303 .map(|s| DateTimeField::new(s.into_owned()))
304 }
305
306 /// Whether the concept is stale at `now`: `now >= stale_after` (§5.5).
307 /// A concept with no (or an unreadable / offset-less) `stale_after` is never stale.
308 #[must_use]
309 pub fn is_stale_at(&self, now: DateTime) -> bool {
310 let Some(stale_after) = self.stale_after() else {
311 return false;
312 };
313 if !stale_after.is_valid() {
314 return false;
315 }
316 trust::is_stale_at(stale_after.datetime, now)
317 }
318
319 /// Whether the concept is stale on `today`: `today >= stale_after` (§5.5).
320 /// Evaluates staleness at midnight UTC on `today`.
321 #[must_use]
322 pub fn is_stale_on(&self, today: Date) -> bool {
323 self.is_stale_at(today.to_utc_datetime())
324 }
325
326 /// `true` when `type` is `Attested Computation` (§10.1).
327 #[must_use]
328 pub fn is_attested_computation(&self) -> bool {
329 self.type_().as_deref() == Some(ATTESTED_COMPUTATION_TYPE)
330 }
331
332 /// The `runtime`: how to run the computation, and so what `parameters`
333 /// mean. REQUIRED on an Attested Computation concept.
334 #[must_use]
335 pub fn runtime(&self) -> Option<Cow<'_, str>> {
336 self.display_str("runtime")
337 }
338
339 /// The declared `parameters` an agent may fill.
340 pub fn parameters(&self) -> Vec<Parameter> {
341 self.map
342 .get("parameters")
343 .map(Parameter::list_from_value)
344 .unwrap_or_default()
345 }
346
347 /// The `computation` path, when the computation lives in a file rather than
348 /// a body block (§10.3).
349 #[must_use]
350 pub fn computation(&self) -> Option<Cow<'_, str>> {
351 self.display_str("computation")
352 }
353
354 /// The `executor`: how the computation is run, and what a receipt carries.
355 pub fn executor(&self) -> Option<Executor> {
356 self.map.get("executor").and_then(Executor::from_value)
357 }
358
359 /// The `attester`: deterministic code that turns a receipt into a verdict.
360 pub fn attester(&self) -> Option<Attester> {
361 self.map.get("attester").and_then(Attester::from_value)
362 }
363
364 /// The path-valued frontmatter fields present (§6.2), as
365 /// `(field name, raw value)`.
366 ///
367 /// `sources[].resource` is deliberately excluded: it may be a scope
368 /// descriptor rather than a path (§5.1). Use
369 /// [`Source::resource_kind`](crate::provenance::Source::resource_kind) to
370 /// filter those yourself.
371 #[must_use]
372 pub fn path_fields(&self) -> Vec<(&'static str, String)> {
373 let mut out = Vec::new();
374 let mut push = |name: &'static str, value: Option<String>| {
375 if let Some(v) = value.filter(|v| !v.trim().is_empty()) {
376 out.push((name, v));
377 }
378 };
379 push(
380 "resource",
381 self.resource().map(std::borrow::Cow::into_owned),
382 );
383 push(
384 "computation",
385 self.computation().map(std::borrow::Cow::into_owned),
386 );
387 push(
388 "executor.resource",
389 self.executor().and_then(|e| e.resource),
390 );
391 push(
392 "attester.resource",
393 self.attester().and_then(|a| a.resource),
394 );
395 out
396 }
397
398 /// Returns the keys present that are not well-known OKF fields, i.e. the
399 /// producer-defined extension keys consumers must preserve (§4.1).
400 #[must_use]
401 pub fn extension_keys(&self) -> Vec<&str> {
402 self.map
403 .keys()
404 .filter(|k| !KNOWN_FRONTMATTER_KEYS.contains(k))
405 .collect()
406 }
407
408 /// Returns the legacy v0.1 keys present that v0.2 supersedes (§13.1).
409 #[must_use]
410 pub fn legacy_keys(&self) -> Vec<&str> {
411 self.map
412 .keys()
413 .filter(|k| LEGACY_FRONTMATTER_KEYS.contains(k))
414 .collect()
415 }
416
417 /// Borrows the scalar at `key` as a display string, coercing non-string
418 /// scalars (a `type: 42` deviation yields `Some("42")`) the way the
419 /// reference's `str(fm.get(...))` does. Returns `None` for absent keys
420 /// and non-scalar values. The common YAML-string case borrows without
421 /// allocation; only the coerced case owns.
422 fn display_str(&self, key: &str) -> Option<Cow<'_, str>> {
423 self.map.get(key).and_then(Value::as_display_str)
424 }
425}
426
427impl From<Mapping> for Frontmatter {
428 fn from(map: Mapping) -> Self {
429 Self { map }
430 }
431}