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    /// Returns the mutable mapping if this is a [`Value::Mapping`].
259    pub const fn as_mapping_mut(&mut self) -> Option<&mut Mapping> {
260        match self {
261            Self::Mapping(m) => Some(m),
262            _ => None,
263        }
264    }
265
266    /// Returns the mutable sequence if this is a [`Value::Sequence`].
267    pub const fn as_sequence_mut(&mut self) -> Option<&mut Vec<Self>> {
268        match self {
269            Self::Sequence(s) => Some(s),
270            _ => None,
271        }
272    }
273
274    /// True for `Null`, an empty string, an empty sequence, or an empty
275    /// mapping. Mirrors Python's "falsy" check used by the reference
276    /// implementation's `validate()` (`not frontmatter.get(k)`).
277    #[must_use]
278    pub const fn is_empty_value(&self) -> bool {
279        match self {
280            Self::Null | Self::Bool(false) | Self::Int(0) => true,
281            Self::String(s) => s.is_empty(),
282            Self::Sequence(s) => s.is_empty(),
283            Self::Mapping(m) => m.is_empty(),
284            _ => false,
285        }
286    }
287
288    /// Renders a scalar as a plain display string (used for typed frontmatter
289    /// accessors that coerce scalars to text, matching the reference's
290    /// `str(fm.get(...))`).
291    #[must_use]
292    pub fn as_display_string(&self) -> Option<String> {
293        match self {
294            Self::String(s) => Some(s.clone()),
295            Self::Bool(b) => Some(b.to_string()),
296            Self::Int(i) => Some(i.to_string()),
297            Self::Float(f) => Some(format!("{f}")),
298            _ => None,
299        }
300    }
301
302    /// The borrowing form of [`as_display_string`](Self::as_display_string):
303    /// returns a [`std::borrow::Cow`] borrowing the [`String`](Self::String) case and
304    /// owning the coerced form for [`Bool`](Self::Bool)/[`Int`](Self::Int)/
305    /// [`Float`](Self::Float). `None` for non-scalar variants.
306    ///
307    /// Frontmatter accessors use this so the common case (a YAML string) is
308    /// allocation-free, while the deviation case (e.g. `type: 42`) still
309    /// coerces to text the way the reference's `str(fm.get(...))` does, rather
310    /// than silently reading as `None`.
311    #[must_use]
312    pub fn as_display_str(&self) -> Option<std::borrow::Cow<'_, str>> {
313        match self {
314            Self::String(s) => Some(std::borrow::Cow::Borrowed(s)),
315            Self::Bool(b) => Some(std::borrow::Cow::Owned(b.to_string())),
316            Self::Int(i) => Some(std::borrow::Cow::Owned(i.to_string())),
317            Self::Float(f) => Some(std::borrow::Cow::Owned(format!("{f}"))),
318            _ => None,
319        }
320    }
321}
322
323impl fmt::Display for Value {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        f.write_str(&self.to_yaml_string())
326    }
327}
328
329impl From<&str> for Value {
330    fn from(s: &str) -> Self {
331        Self::String(s.to_string())
332    }
333}
334
335impl From<String> for Value {
336    fn from(s: String) -> Self {
337        Self::String(s)
338    }
339}
340
341impl From<bool> for Value {
342    fn from(b: bool) -> Self {
343        Self::Bool(b)
344    }
345}
346
347impl From<i64> for Value {
348    fn from(i: i64) -> Self {
349        Self::Int(i)
350    }
351}
352
353impl<T: Into<Self>> From<Vec<T>> for Value {
354    fn from(v: Vec<T>) -> Self {
355        Self::Sequence(v.into_iter().map(Into::into).collect())
356    }
357}
358
359impl From<Mapping> for Value {
360    fn from(m: Mapping) -> Self {
361        Self::Mapping(m)
362    }
363}
364
365impl std::str::FromStr for Value {
366    type Err = YamlError;
367    fn from_str(s: &str) -> Result<Self, Self::Err> {
368        Self::parse(s)
369    }
370}
371
372impl From<i32> for Value {
373    fn from(i: i32) -> Self {
374        Self::Int(i64::from(i))
375    }
376}
377
378impl From<i16> for Value {
379    fn from(i: i16) -> Self {
380        Self::Int(i64::from(i))
381    }
382}
383
384impl From<i8> for Value {
385    fn from(i: i8) -> Self {
386        Self::Int(i64::from(i))
387    }
388}
389
390impl From<u32> for Value {
391    fn from(u: u32) -> Self {
392        Self::Int(i64::from(u))
393    }
394}
395
396impl From<u16> for Value {
397    fn from(u: u16) -> Self {
398        Self::Int(i64::from(u))
399    }
400}
401
402impl From<u8> for Value {
403    fn from(u: u8) -> Self {
404        Self::Int(i64::from(u))
405    }
406}
407
408impl From<u64> for Value {
409    fn from(u: u64) -> Self {
410        Self::Int(i64::try_from(u).unwrap_or(i64::MAX))
411    }
412}
413
414impl From<usize> for Value {
415    fn from(u: usize) -> Self {
416        Self::Int(i64::try_from(u).unwrap_or(i64::MAX))
417    }
418}
419
420impl From<f64> for Value {
421    fn from(f: f64) -> Self {
422        Self::Float(f)
423    }
424}
425
426impl From<f32> for Value {
427    fn from(f: f32) -> Self {
428        Self::Float(f64::from(f))
429    }
430}
431
432impl From<&String> for Value {
433    fn from(s: &String) -> Self {
434        Self::String(s.clone())
435    }
436}
437
438impl From<std::borrow::Cow<'_, str>> for Value {
439    fn from(s: std::borrow::Cow<'_, str>) -> Self {
440        Self::String(s.into_owned())
441    }
442}
443
444impl From<()> for Value {
445    fn from((): ()) -> Self {
446        Self::Null
447    }
448}
449
450impl<T: Into<Self>> From<Option<T>> for Value {
451    fn from(opt: Option<T>) -> Self {
452        opt.map_or(Self::Null, Into::into)
453    }
454}
455
456impl fmt::Display for Mapping {
457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458        f.write_str(&Value::Mapping(self.clone()).to_yaml_string())
459    }
460}
461
462impl IntoIterator for Mapping {
463    type Item = (Value, Value);
464    type IntoIter = std::vec::IntoIter<(Value, Value)>;
465    fn into_iter(self) -> Self::IntoIter {
466        self.entries.into_iter()
467    }
468}
469
470impl<'a> IntoIterator for &'a Mapping {
471    type Item = (&'a Value, &'a Value);
472    type IntoIter = std::iter::Map<
473        std::slice::Iter<'a, (Value, Value)>,
474        fn(&(Value, Value)) -> (&Value, &Value),
475    >;
476    fn into_iter(self) -> Self::IntoIter {
477        const fn map_ref(entry: &(Value, Value)) -> (&Value, &Value) {
478            (&entry.0, &entry.1)
479        }
480        self.entries.iter().map(map_ref)
481    }
482}
483
484impl<'a> IntoIterator for &'a mut Mapping {
485    type Item = (&'a mut Value, &'a mut Value);
486    type IntoIter = std::iter::Map<
487        std::slice::IterMut<'a, (Value, Value)>,
488        fn(&mut (Value, Value)) -> (&mut Value, &mut Value),
489    >;
490    fn into_iter(self) -> Self::IntoIter {
491        const fn map_mut(entry: &mut (Value, Value)) -> (&mut Value, &mut Value) {
492            (&mut entry.0, &mut entry.1)
493        }
494        self.entries.iter_mut().map(map_mut)
495    }
496}
497
498impl FromIterator<(Value, Value)> for Mapping {
499    fn from_iter<T: IntoIterator<Item = (Value, Value)>>(iter: T) -> Self {
500        Self {
501            entries: iter.into_iter().collect(),
502        }
503    }
504}
505
506impl FromIterator<(String, Value)> for Mapping {
507    fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
508        let mut map = Self::new();
509        for (k, v) in iter {
510            map.insert(k, v);
511        }
512        map
513    }
514}
515
516impl<'a> FromIterator<(&'a str, Value)> for Mapping {
517    fn from_iter<T: IntoIterator<Item = (&'a str, Value)>>(iter: T) -> Self {
518        let mut map = Self::new();
519        for (k, v) in iter {
520            map.insert(k, v);
521        }
522        map
523    }
524}
525
526impl Extend<(Value, Value)> for Mapping {
527    fn extend<T: IntoIterator<Item = (Value, Value)>>(&mut self, iter: T) {
528        self.entries.extend(iter);
529    }
530}
531
532impl Extend<(String, Value)> for Mapping {
533    fn extend<T: IntoIterator<Item = (String, Value)>>(&mut self, iter: T) {
534        for (k, v) in iter {
535            self.insert(k, v);
536        }
537    }
538}
539
540impl<'a> Extend<(&'a str, Value)> for Mapping {
541    fn extend<T: IntoIterator<Item = (&'a str, Value)>>(&mut self, iter: T) {
542        for (k, v) in iter {
543            self.insert(k, v);
544        }
545    }
546}