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 (§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}