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}