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::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: a concept carrying
34/// nothing but `type` is fully conformant.
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 recommended fields; `generated` is the
47/// record of how the content was produced. The spec 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.
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/// `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.
61pub const KNOWN_FRONTMATTER_KEYS: [&str; 17] = [
62    // Core.
63    "type",
64    "title",
65    "description",
66    "resource",
67    "tags",
68    // Provenance.
69    "sources",
70    "usage_window",
71    // Trust.
72    "generated",
73    "verified",
74    // Lifecycle.
75    "status",
76    "stale_after",
77    // Attested Computation.
78    "runtime",
79    "parameters",
80    "computation",
81    "executor",
82    "attester",
83    // Legacy.
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. Frontmatter has 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    /// Removes a key from frontmatter, returning the removed value if present.
164    pub fn remove(&mut self, key: &str) -> Option<Value> {
165        self.map.remove(key)
166    }
167
168    /// Reorders the keys into [`PREFERRED_KEY_ORDER`], leaving every other key
169    /// after them in its current relative order.
170    ///
171    /// A port of the reference implementation's `_reorder_frontmatter`, which it
172    /// applies whenever it writes a concept document; call this before
173    /// [`Document::serialize`](crate::Document::serialize) to produce
174    /// frontmatter laid out the same way. No key is added, dropped, or
175    /// rewritten, so only the serialized order changes.
176    pub fn reorder_preferred(&mut self) {
177        let mut ordered = Mapping::new();
178        for key in PREFERRED_KEY_ORDER {
179            if let Some(value) = self.map.get(key) {
180                ordered.insert(key, value.clone());
181            }
182        }
183        for (key, value) in self.map.iter() {
184            let already_placed = key
185                .as_str()
186                .is_some_and(|k| PREFERRED_KEY_ORDER.contains(&k));
187            if !already_placed {
188                ordered.push_raw(key.clone(), value.clone());
189            }
190        }
191        self.map = ordered;
192    }
193
194    /// The **required** `type` field. `None` if absent or not a scalar.
195    ///
196    /// Non-string scalars (`type: 42`) are coerced to their display form, the
197    /// way the reference's `str(fm.get("type"))` does, rather than read as
198    /// `None`: the spec calls `type` "a short string", so a non-string value
199    /// is a producer deviation, but a consumer still gets *something* to
200    /// route on rather than treating the concept as typeless.
201    #[must_use]
202    pub fn type_(&self) -> Option<Cow<'_, str>> {
203        self.display_str("type")
204    }
205
206    /// The optional `title` field.
207    #[must_use]
208    pub fn title(&self) -> Option<Cow<'_, str>> {
209        self.display_str("title")
210    }
211
212    /// The optional one-line `description`.
213    #[must_use]
214    pub fn description(&self) -> Option<Cow<'_, str>> {
215        self.display_str("description")
216    }
217
218    /// The optional `resource` URI for the underlying asset.
219    #[must_use]
220    pub fn resource(&self) -> Option<Cow<'_, str>> {
221        self.display_str("resource")
222    }
223
224    /// The optional `tags` list. Non-string elements are coerced to their
225    /// display form; a non-sequence `tags` value yields an empty vector.
226    pub fn tags(&self) -> Vec<String> {
227        match self.map.get("tags") {
228            Some(Value::Sequence(items)) => {
229                items.iter().filter_map(Value::as_display_string).collect()
230            }
231            _ => Vec::new(),
232        }
233    }
234
235    /// The `sources` entries: the materials this concept derives from.
236    pub fn sources(&self) -> Vec<Source> {
237        self.map
238            .get("sources")
239            .map(Source::list_from_value)
240            .unwrap_or_default()
241    }
242
243    /// The shared `usage_window` that frames every `sources[].usage_count`.
244    pub fn usage_window(&self) -> Option<UsageWindow> {
245        self.map
246            .get("usage_window")
247            .and_then(UsageWindow::from_value)
248    }
249
250    /// The `generated` block: how the current content was produced.
251    pub fn generated(&self) -> Option<Generated> {
252        self.map.get("generated").and_then(Generated::from_value)
253    }
254
255    /// The `verified` events: who or what has confirmed this content.
256    ///
257    /// A bare `{ by, at }` mapping is returned as a one-element list, as the
258    /// spec requires.
259    pub fn verified(&self) -> Vec<Verification> {
260        self.map
261            .get("verified")
262            .map(Verification::list_from_value)
263            .unwrap_or_default()
264    }
265
266    /// The verification with the latest parseable `at`.
267    #[must_use]
268    pub fn latest_verification(&self) -> Option<Verification> {
269        let events = self.verified();
270        trust::latest_verification(&events).cloned()
271    }
272
273    /// The trust tier derived from `verified`.
274    #[must_use]
275    pub fn trust_tier(&self) -> TrustTier {
276        TrustTier::derive(&self.verified())
277    }
278
279    /// When the content last meaningfully changed: `generated.at`,
280    /// falling back to a legacy v0.1 `timestamp` when `generated` is absent, as
281    /// permitted.
282    #[must_use]
283    pub fn content_changed_at(&self) -> Option<DateTimeField> {
284        self.generated()
285            .and_then(|g| g.at)
286            .or_else(|| self.timestamp().map(|s| DateTimeField::new(s.into_owned())))
287    }
288
289    /// The legacy v0.1 `timestamp` field, superseded by `generated.at`.
290    ///
291    /// Prefer [`Frontmatter::content_changed_at`], which reads `generated.at`
292    /// first and falls back to this.
293    #[must_use]
294    pub fn timestamp(&self) -> Option<Cow<'_, str>> {
295        self.display_str("timestamp")
296    }
297
298    /// The lifecycle `status`. An absent key is [`Status::Stable`].
299    #[must_use]
300    pub fn status(&self) -> Status {
301        Status::parse(self.display_str("status").as_deref())
302    }
303
304    /// The `stale_after` timestamp, on and after which the content is stale.
305    #[must_use]
306    pub fn stale_after(&self) -> Option<DateTimeField> {
307        self.display_str("stale_after")
308            .map(|s| DateTimeField::new(s.into_owned()))
309    }
310
311    /// Whether the concept is stale at `now`: `now >= stale_after`.
312    /// A concept with no (or an unreadable / offset-less) `stale_after` is never stale.
313    #[must_use]
314    pub fn is_stale_at(&self, now: DateTime) -> bool {
315        let Some(stale_after) = self.stale_after() else {
316            return false;
317        };
318        if !stale_after.is_valid() {
319            return false;
320        }
321        trust::is_stale_at(stale_after.datetime, now)
322    }
323
324    /// Whether the concept is stale on `today`: `today >= stale_after`.
325    /// Evaluates staleness at midnight UTC on `today`.
326    #[must_use]
327    pub fn is_stale_on(&self, today: Date) -> bool {
328        self.is_stale_at(today.to_utc_datetime())
329    }
330
331    /// `true` when `type` is `Attested Computation`.
332    #[must_use]
333    pub fn is_attested_computation(&self) -> bool {
334        self.type_().as_deref() == Some(ATTESTED_COMPUTATION_TYPE)
335    }
336
337    /// The `runtime`: how to run the computation, and so what `parameters`
338    /// mean. REQUIRED on an Attested Computation concept.
339    #[must_use]
340    pub fn runtime(&self) -> Option<Cow<'_, str>> {
341        self.display_str("runtime")
342    }
343
344    /// The declared `parameters` an agent may fill.
345    pub fn parameters(&self) -> Vec<Parameter> {
346        self.map
347            .get("parameters")
348            .map(Parameter::list_from_value)
349            .unwrap_or_default()
350    }
351
352    /// The `computation` path, when the computation lives in a file rather than
353    /// a body block.
354    #[must_use]
355    pub fn computation(&self) -> Option<Cow<'_, str>> {
356        self.display_str("computation")
357    }
358
359    /// The `executor`: how the computation is run, and what a receipt carries.
360    pub fn executor(&self) -> Option<Executor> {
361        self.map.get("executor").and_then(Executor::from_value)
362    }
363
364    /// The `attester`: deterministic code that turns a receipt into a verdict.
365    pub fn attester(&self) -> Option<Attester> {
366        self.map.get("attester").and_then(Attester::from_value)
367    }
368
369    /// The path-valued frontmatter fields present, as
370    /// `(field name, raw value)`.
371    ///
372    /// `sources[].resource` is deliberately excluded: it may be a scope
373    /// descriptor rather than a path. Use
374    /// [`Source::resource_kind`](crate::provenance::Source::resource_kind) to
375    /// filter those yourself.
376    #[must_use]
377    pub fn path_fields(&self) -> Vec<(&'static str, String)> {
378        let mut out = Vec::new();
379        let mut push = |name: &'static str, value: Option<String>| {
380            if let Some(v) = value.filter(|v| !v.trim().is_empty()) {
381                out.push((name, v));
382            }
383        };
384        push(
385            "resource",
386            self.resource().map(std::borrow::Cow::into_owned),
387        );
388        push(
389            "computation",
390            self.computation().map(std::borrow::Cow::into_owned),
391        );
392        push(
393            "executor.resource",
394            self.executor().and_then(|e| e.resource),
395        );
396        push(
397            "attester.resource",
398            self.attester().and_then(|a| a.resource),
399        );
400        out
401    }
402
403    /// Returns the keys present that are not well-known OKF fields, i.e. the
404    /// producer-defined extension keys consumers must preserve.
405    #[must_use]
406    pub fn extension_keys(&self) -> Vec<&str> {
407        self.map
408            .keys()
409            .filter(|k| !KNOWN_FRONTMATTER_KEYS.contains(k))
410            .collect()
411    }
412
413    /// Returns the legacy v0.1 keys present that v0.2 supersedes.
414    #[must_use]
415    pub fn legacy_keys(&self) -> Vec<&str> {
416        self.map
417            .keys()
418            .filter(|k| LEGACY_FRONTMATTER_KEYS.contains(k))
419            .collect()
420    }
421
422    /// Borrows the scalar at `key` as a display string, coercing non-string
423    /// scalars (a `type: 42` deviation yields `Some("42")`) the way the
424    /// reference's `str(fm.get(...))` does. Returns `None` for absent keys
425    /// and non-scalar values. The common YAML-string case borrows without
426    /// allocation; only the coerced case owns.
427    fn display_str(&self, key: &str) -> Option<Cow<'_, str>> {
428        self.map.get(key).and_then(Value::as_display_str)
429    }
430}
431
432impl From<Mapping> for Frontmatter {
433    fn from(map: Mapping) -> Self {
434        Self { map }
435    }
436}