Skip to main content

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