Skip to main content

okf_core/yaml/
mod.rs

1//! A small YAML *subset* parser used for OKF frontmatter.
2//!
3//! OKF frontmatter is, in practice, a flat-ish YAML mapping of scalars, lists,
4//! and occasionally nested mappings (see the [specification][spec]). A
5//! full YAML 1.2 engine would be overkill and would pull in dependencies, so
6//! this module implements the pragmatic subset that real frontmatter uses:
7//!
8//! - block mappings (`key: value`), including nested/indented blocks;
9//! - block sequences (`- item`);
10//! - flow collections (`[a, b]`, `{a: 1, b: 2}`);
11//! - plain, single-quoted, and double-quoted scalars;
12//! - literal (`|`) and folded (`>`) block scalars;
13//! - `#` comments and blank lines;
14//! - the core scalar types: null, bool, int, float, string.
15//!
16//! Plain and quoted scalars may span lines, folding each break into a single
17//! space, because `PyYAML`'s `safe_dump` wraps any value past its 80-column line
18//! width and the reference implementation publishes bundles that way.
19//!
20//! It deliberately does **not** support anchors/aliases, explicit tags
21//! (`!!str`), multiple documents, or complex (non-scalar) mapping keys. Those
22//! never appear in well-formed OKF frontmatter; encountering them yields a
23//! clear [`YamlError`] rather than silent misbehaviour.
24//!
25//! The guarantee that matters for OKF round-tripping is:
26//! `parse(emit(parse(x))) == parse(x)`. Emitting and re-parsing preserves the
27//! logical value and key order. This mirrors the reference implementation's
28//! `OKFDocument` round-trip test.
29//!
30//! ## Timestamps are strings
31//!
32//! One deliberate divergence from `PyYAML`: YAML's implicit resolver types a bare
33//! `2026-12-31` as a date and a bare `2026-06-30T14:00:00Z` as a datetime, while
34//! this module keeps every scalar of either shape as a string. The OKF layer
35//! loses nothing, since [`DateField`](crate::DateField) and
36//! [`DateTimeField`](crate::DateTimeField) keep the text beside the parsed
37//! value, and it means a malformed date can be reported rather than silently
38//! dropped.
39//!
40//! The consequence shows up on the way out. A bare ISO datetime is not stable
41//! even under the reference's own round-trip: `PyYAML` loads it into a `datetime`
42//! and dumps it back as `2026-06-30 14:00:00+00:00`, losing the `T` and `Z`
43//! separators the spec asks for. A quoted one survives byte-identical. So the
44//! emitter quotes a datetime-valued string and leaves a bare `YYYY-MM-DD` plain,
45//! which is how both the specification and the reference write `stale_after`,
46//! `last_modified`, and `usage_window`.
47//!
48//! [spec]: https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md
49
50mod emitter;
51mod parser;
52
53use std::fmt;
54
55pub use parser::YamlError;
56
57/// An ordered YAML mapping (preserves insertion / source order, like the
58/// reference implementation which dumps with `sort_keys=False`).
59///
60/// Keys are [`Value`]s for generality, but OKF frontmatter keys are always
61/// strings; the [`get`](Mapping::get) / [`insert`](Mapping::insert) helpers
62/// operate on string keys for convenience.
63#[derive(Clone, Debug, Default, PartialEq)]
64pub struct Mapping {
65    entries: Vec<(Value, Value)>,
66}
67
68impl Mapping {
69    /// Creates an empty mapping.
70    #[must_use]
71    pub const fn new() -> Self {
72        Self {
73            entries: Vec::new(),
74        }
75    }
76
77    /// Number of key/value pairs.
78    #[must_use]
79    pub const fn len(&self) -> usize {
80        self.entries.len()
81    }
82
83    /// Returns `true` if the mapping has no entries.
84    #[must_use]
85    pub const fn is_empty(&self) -> bool {
86        self.entries.is_empty()
87    }
88
89    /// Looks up a value by string key.
90    #[must_use]
91    pub fn get(&self, key: &str) -> Option<&Value> {
92        self.entries
93            .iter()
94            .find(|(k, _)| k.as_str() == Some(key))
95            .map(|(_, v)| v)
96    }
97
98    /// Looks up a mutable value by string key.
99    pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
100        self.entries
101            .iter_mut()
102            .find(|(k, _)| k.as_str() == Some(key))
103            .map(|(_, v)| v)
104    }
105
106    /// Returns `true` if the mapping contains the given string key.
107    #[must_use]
108    pub fn contains_key(&self, key: &str) -> bool {
109        self.get(key).is_some()
110    }
111
112    /// Inserts (or, if the string key already exists, replaces) a value,
113    /// preserving the position of an existing key. Returns the previous value.
114    pub fn insert(&mut self, key: impl Into<String>, value: Value) -> Option<Value> {
115        let key = key.into();
116        if let Some(slot) = self
117            .entries
118            .iter_mut()
119            .find(|(k, _)| k.as_str() == Some(&key))
120        {
121            return Some(std::mem::replace(&mut slot.1, value));
122        }
123        self.entries.push((Value::String(key), value));
124        None
125    }
126
127    /// Removes a value by string key, preserving order of the rest.
128    pub fn remove(&mut self, key: &str) -> Option<Value> {
129        let idx = self
130            .entries
131            .iter()
132            .position(|(k, _)| k.as_str() == Some(key))?;
133        Some(self.entries.remove(idx).1)
134    }
135
136    /// Pushes a raw key/value pair (used by the parser; keeps non-string keys).
137    pub(crate) fn push_raw(&mut self, key: Value, value: Value) {
138        self.entries.push((key, value));
139    }
140
141    /// Iterates over `(key, value)` pairs in order.
142    pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> {
143        self.entries.iter().map(|(k, v)| (k, v))
144    }
145
146    /// Iterates over mutable `(key, value)` pairs in order.
147    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut Value, &mut Value)> {
148        self.entries.iter_mut().map(|(k, v)| (k, v))
149    }
150
151    /// Iterates over string keys (skipping any non-string keys).
152    pub fn keys(&self) -> impl Iterator<Item = &str> {
153        self.entries.iter().filter_map(|(k, _)| k.as_str())
154    }
155
156    /// Iterates over values in order.
157    pub fn values(&self) -> impl Iterator<Item = &Value> {
158        self.entries.iter().map(|(_, v)| v)
159    }
160
161    /// Borrows the underlying slice of key-value entry pairs.
162    #[must_use]
163    pub fn entries(&self) -> &[(Value, Value)] {
164        &self.entries
165    }
166}
167
168/// A parsed YAML value.
169#[derive(Clone, Debug, PartialEq)]
170pub enum Value {
171    /// `null`, `~`, or an empty value.
172    Null,
173    /// `true` / `false`.
174    Bool(bool),
175    /// An integer scalar.
176    Int(i64),
177    /// A floating-point scalar.
178    Float(f64),
179    /// A string scalar.
180    String(String),
181    /// A sequence (`[...]` or block `- ...`).
182    Sequence(Vec<Self>),
183    /// A mapping (`{...}` or block `key: value`).
184    Mapping(Mapping),
185}
186
187impl Value {
188    /// Parses a single YAML value from text (the OKF frontmatter subset).
189    ///
190    /// # Errors
191    ///
192    /// Returns [`YamlError`] for any input outside the supported subset
193    /// (anchors, tags, multiple documents, or syntactically malformed YAML).
194    pub fn parse(text: &str) -> Result<Self, YamlError> {
195        parser::parse(text)
196    }
197
198    /// Emits this value as YAML text using block style, preserving key order.
199    #[must_use]
200    pub fn to_yaml_string(&self) -> String {
201        emitter::emit(self)
202    }
203
204    /// Returns the string contents if this is a [`Value::String`].
205    #[must_use]
206    pub fn as_str(&self) -> Option<&str> {
207        match self {
208            Self::String(s) => Some(s),
209            _ => None,
210        }
211    }
212
213    /// Returns the boolean if this is a [`Value::Bool`].
214    #[must_use]
215    pub const fn as_bool(&self) -> Option<bool> {
216        match self {
217            Self::Bool(b) => Some(*b),
218            _ => None,
219        }
220    }
221
222    /// Returns the integer if this is a [`Value::Int`].
223    #[must_use]
224    pub const fn as_int(&self) -> Option<i64> {
225        match self {
226            Self::Int(i) => Some(*i),
227            _ => None,
228        }
229    }
230
231    /// Returns the floating-point number if this is a [`Value::Float`].
232    #[must_use]
233    pub const fn as_float(&self) -> Option<f64> {
234        match self {
235            Self::Float(f) => Some(*f),
236            _ => None,
237        }
238    }
239
240    /// Returns the sequence elements if this is a [`Value::Sequence`].
241    #[must_use]
242    pub fn as_sequence(&self) -> Option<&[Self]> {
243        match self {
244            Self::Sequence(s) => Some(s),
245            _ => None,
246        }
247    }
248
249    /// Returns the mapping if this is a [`Value::Mapping`].
250    #[must_use]
251    pub const fn as_mapping(&self) -> Option<&Mapping> {
252        match self {
253            Self::Mapping(m) => Some(m),
254            _ => None,
255        }
256    }
257
258    /// True for `Null`, an empty string, an empty sequence, or an empty
259    /// mapping. Mirrors Python's "falsy" check used by the reference
260    /// implementation's `validate()` (`not frontmatter.get(k)`).
261    #[must_use]
262    pub const fn is_empty_value(&self) -> bool {
263        match self {
264            Self::Null | Self::Bool(false) | Self::Int(0) => true,
265            Self::String(s) => s.is_empty(),
266            Self::Sequence(s) => s.is_empty(),
267            Self::Mapping(m) => m.is_empty(),
268            _ => false,
269        }
270    }
271
272    /// Renders a scalar as a plain display string (used for typed frontmatter
273    /// accessors that coerce scalars to text, matching the reference's
274    /// `str(fm.get(...))`).
275    #[must_use]
276    pub fn as_display_string(&self) -> Option<String> {
277        match self {
278            Self::String(s) => Some(s.clone()),
279            Self::Bool(b) => Some(b.to_string()),
280            Self::Int(i) => Some(i.to_string()),
281            Self::Float(f) => Some(format!("{f}")),
282            _ => None,
283        }
284    }
285
286    /// The borrowing form of [`as_display_string`](Self::as_display_string):
287    /// returns a [`std::borrow::Cow`] borrowing the [`String`](Self::String) case and
288    /// owning the coerced form for [`Bool`](Self::Bool)/[`Int`](Self::Int)/
289    /// [`Float`](Self::Float). `None` for non-scalar variants.
290    ///
291    /// Frontmatter accessors use this so the common case (a YAML string) is
292    /// allocation-free, while the deviation case (e.g. `type: 42`) still
293    /// coerces to text the way the reference's `str(fm.get(...))` does, rather
294    /// than silently reading as `None`.
295    #[must_use]
296    pub fn as_display_str(&self) -> Option<std::borrow::Cow<'_, str>> {
297        match self {
298            Self::String(s) => Some(std::borrow::Cow::Borrowed(s)),
299            Self::Bool(b) => Some(std::borrow::Cow::Owned(b.to_string())),
300            Self::Int(i) => Some(std::borrow::Cow::Owned(i.to_string())),
301            Self::Float(f) => Some(std::borrow::Cow::Owned(format!("{f}"))),
302            _ => None,
303        }
304    }
305}
306
307impl fmt::Display for Value {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        f.write_str(&self.to_yaml_string())
310    }
311}
312
313impl From<&str> for Value {
314    fn from(s: &str) -> Self {
315        Self::String(s.to_string())
316    }
317}
318
319impl From<String> for Value {
320    fn from(s: String) -> Self {
321        Self::String(s)
322    }
323}
324
325impl From<bool> for Value {
326    fn from(b: bool) -> Self {
327        Self::Bool(b)
328    }
329}
330
331impl From<i64> for Value {
332    fn from(i: i64) -> Self {
333        Self::Int(i)
334    }
335}
336
337impl<T: Into<Self>> From<Vec<T>> for Value {
338    fn from(v: Vec<T>) -> Self {
339        Self::Sequence(v.into_iter().map(Into::into).collect())
340    }
341}
342
343impl From<Mapping> for Value {
344    fn from(m: Mapping) -> Self {
345        Self::Mapping(m)
346    }
347}
348
349impl std::str::FromStr for Value {
350    type Err = YamlError;
351    fn from_str(s: &str) -> Result<Self, Self::Err> {
352        Self::parse(s)
353    }
354}
355
356impl From<i32> for Value {
357    fn from(i: i32) -> Self {
358        Self::Int(i64::from(i))
359    }
360}
361
362impl From<i16> for Value {
363    fn from(i: i16) -> Self {
364        Self::Int(i64::from(i))
365    }
366}
367
368impl From<i8> for Value {
369    fn from(i: i8) -> Self {
370        Self::Int(i64::from(i))
371    }
372}
373
374impl From<u32> for Value {
375    fn from(u: u32) -> Self {
376        Self::Int(i64::from(u))
377    }
378}
379
380impl From<u16> for Value {
381    fn from(u: u16) -> Self {
382        Self::Int(i64::from(u))
383    }
384}
385
386impl From<u8> for Value {
387    fn from(u: u8) -> Self {
388        Self::Int(i64::from(u))
389    }
390}
391
392impl From<u64> for Value {
393    fn from(u: u64) -> Self {
394        Self::Int(i64::try_from(u).unwrap_or(i64::MAX))
395    }
396}
397
398impl From<usize> for Value {
399    fn from(u: usize) -> Self {
400        Self::Int(i64::try_from(u).unwrap_or(i64::MAX))
401    }
402}
403
404impl From<f64> for Value {
405    fn from(f: f64) -> Self {
406        Self::Float(f)
407    }
408}
409
410impl From<f32> for Value {
411    fn from(f: f32) -> Self {
412        Self::Float(f64::from(f))
413    }
414}
415
416impl From<&String> for Value {
417    fn from(s: &String) -> Self {
418        Self::String(s.clone())
419    }
420}
421
422impl From<std::borrow::Cow<'_, str>> for Value {
423    fn from(s: std::borrow::Cow<'_, str>) -> Self {
424        Self::String(s.into_owned())
425    }
426}
427
428impl From<()> for Value {
429    fn from((): ()) -> Self {
430        Self::Null
431    }
432}
433
434impl<T: Into<Self>> From<Option<T>> for Value {
435    fn from(opt: Option<T>) -> Self {
436        opt.map_or(Self::Null, Into::into)
437    }
438}
439
440impl fmt::Display for Mapping {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        f.write_str(&Value::Mapping(self.clone()).to_yaml_string())
443    }
444}
445
446impl IntoIterator for Mapping {
447    type Item = (Value, Value);
448    type IntoIter = std::vec::IntoIter<(Value, Value)>;
449    fn into_iter(self) -> Self::IntoIter {
450        self.entries.into_iter()
451    }
452}
453
454impl<'a> IntoIterator for &'a Mapping {
455    type Item = (&'a Value, &'a Value);
456    type IntoIter = std::iter::Map<
457        std::slice::Iter<'a, (Value, Value)>,
458        fn(&(Value, Value)) -> (&Value, &Value),
459    >;
460    fn into_iter(self) -> Self::IntoIter {
461        const fn map_ref(entry: &(Value, Value)) -> (&Value, &Value) {
462            (&entry.0, &entry.1)
463        }
464        self.entries.iter().map(map_ref)
465    }
466}
467
468impl<'a> IntoIterator for &'a mut Mapping {
469    type Item = (&'a mut Value, &'a mut Value);
470    type IntoIter = std::iter::Map<
471        std::slice::IterMut<'a, (Value, Value)>,
472        fn(&mut (Value, Value)) -> (&mut Value, &mut Value),
473    >;
474    fn into_iter(self) -> Self::IntoIter {
475        const fn map_mut(entry: &mut (Value, Value)) -> (&mut Value, &mut Value) {
476            (&mut entry.0, &mut entry.1)
477        }
478        self.entries.iter_mut().map(map_mut)
479    }
480}
481
482impl FromIterator<(Value, Value)> for Mapping {
483    fn from_iter<T: IntoIterator<Item = (Value, Value)>>(iter: T) -> Self {
484        Self {
485            entries: iter.into_iter().collect(),
486        }
487    }
488}
489
490impl FromIterator<(String, Value)> for Mapping {
491    fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
492        let mut map = Self::new();
493        for (k, v) in iter {
494            map.insert(k, v);
495        }
496        map
497    }
498}
499
500impl<'a> FromIterator<(&'a str, Value)> for Mapping {
501    fn from_iter<T: IntoIterator<Item = (&'a str, Value)>>(iter: T) -> Self {
502        let mut map = Self::new();
503        for (k, v) in iter {
504            map.insert(k, v);
505        }
506        map
507    }
508}
509
510impl Extend<(Value, Value)> for Mapping {
511    fn extend<T: IntoIterator<Item = (Value, Value)>>(&mut self, iter: T) {
512        self.entries.extend(iter);
513    }
514}
515
516impl Extend<(String, Value)> for Mapping {
517    fn extend<T: IntoIterator<Item = (String, Value)>>(&mut self, iter: T) {
518        for (k, v) in iter {
519            self.insert(k, v);
520        }
521    }
522}
523
524impl<'a> Extend<(&'a str, Value)> for Mapping {
525    fn extend<T: IntoIterator<Item = (&'a str, Value)>>(&mut self, iter: T) {
526        for (k, v) in iter {
527            self.insert(k, v);
528        }
529    }
530}