Skip to main content

noyalib/
document.rs

1//! Multi-document YAML loading.
2//!
3//! This module provides functionality for parsing YAML documents that contain
4//! multiple documents separated by `---`.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use noyalib::document::load_all;
10//!
11//! let yaml = "---
12//! name: doc1
13//! ---
14//! name: doc2
15//! ";
16//!
17//! let docs: Vec<_> = load_all(yaml).unwrap().collect();
18//! assert_eq!(docs.len(), 2);
19//! ```
20
21// SPDX-License-Identifier: MIT OR Apache-2.0
22// Copyright (c) 2026 Noyalib. All rights reserved.
23
24use crate::de::ParserConfig;
25use crate::error::{Error, Result};
26use crate::parser;
27use crate::prelude::*;
28#[cfg(feature = "std")]
29use crate::span_context::{self, SpanTree};
30use crate::value::Value;
31#[cfg(not(feature = "std"))]
32use alloc::vec::IntoIter;
33#[cfg(feature = "std")]
34use core::marker::PhantomData;
35#[cfg(feature = "std")]
36use std::vec::IntoIter;
37
38/// An iterator over YAML documents in a string.
39///
40/// Created by the [`load_all`] function.
41///
42/// # Examples
43///
44/// ```
45/// use noyalib::document::load_all;
46/// let iter = load_all("---\na: 1\n---\nb: 2\n").unwrap();
47/// assert_eq!(iter.len(), 2);
48/// ```
49#[derive(Debug)]
50pub struct DocumentIterator {
51    docs: IntoIter<Value>,
52    #[cfg(feature = "std")]
53    _span_trees: Vec<SpanTree>,
54    total: usize,
55}
56
57impl DocumentIterator {
58    /// Returns the total number of documents parsed.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use noyalib::document::load_all;
64    /// let iter = load_all("a: 1\n").unwrap();
65    /// assert_eq!(iter.len(), 1);
66    /// ```
67    #[must_use]
68    pub fn len(&self) -> usize {
69        self.total
70    }
71
72    /// Returns true if there are no documents.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use noyalib::document::load_all;
78    /// let iter = load_all("a: 1\n").unwrap();
79    /// assert!(!iter.is_empty());
80    /// ```
81    #[must_use]
82    pub fn is_empty(&self) -> bool {
83        self.total == 0
84    }
85}
86
87impl Iterator for DocumentIterator {
88    type Item = Result<Value>;
89
90    fn next(&mut self) -> Option<Self::Item> {
91        self.docs.next().map(Ok)
92    }
93
94    fn size_hint(&self) -> (usize, Option<usize>) {
95        self.docs.size_hint()
96    }
97}
98
99impl ExactSizeIterator for DocumentIterator {}
100
101/// Load all YAML documents from a string.
102///
103/// This function parses a YAML string that may contain multiple documents
104/// separated by `---` markers. Default security limits are applied.
105///
106/// # Examples
107///
108/// ```rust
109/// use noyalib::document::load_all;
110///
111/// let yaml = "---
112/// first: 1
113/// ---
114/// second: 2
115/// ";
116///
117/// let docs: Vec<_> = load_all(yaml).unwrap().filter_map(Result::ok).collect();
118/// assert_eq!(docs.len(), 2);
119/// ```
120///
121/// # Errors
122///
123/// Returns an error if the YAML syntax is invalid.
124pub fn load_all(input: &str) -> Result<DocumentIterator> {
125    load_all_with_config(input, &ParserConfig::default())
126}
127
128/// Load all YAML documents from a string with custom security limits.
129///
130/// # Errors
131///
132/// Returns an error if the YAML syntax is invalid or the document
133/// exceeds the configured limits.
134///
135/// # Examples
136///
137/// ```
138/// use noyalib::{document::load_all_with_config, ParserConfig};
139/// let cfg = ParserConfig::new();
140/// let iter = load_all_with_config("a: 1\n---\nb: 2\n", &cfg).unwrap();
141/// assert_eq!(iter.len(), 2);
142/// ```
143pub fn load_all_with_config(input: &str, config: &ParserConfig) -> Result<DocumentIterator> {
144    if input.len() > config.max_document_length {
145        return Err(Error::Parse(format!(
146            "document exceeds maximum length of {} bytes",
147            config.max_document_length
148        )));
149    }
150    let parse_config = parser::ParseConfig::from(config);
151
152    #[cfg(feature = "std")]
153    {
154        let pairs = parser::parse(input, &parse_config)?;
155        let (docs, span_trees): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
156        let total = docs.len();
157        Ok(DocumentIterator {
158            docs: docs.into_iter(),
159            _span_trees: span_trees,
160            total,
161        })
162    }
163
164    #[cfg(not(feature = "std"))]
165    {
166        let docs = parser::parse_all_values(input, &parse_config)?;
167        let total = docs.len();
168        Ok(DocumentIterator {
169            docs: docs.into_iter(),
170            total,
171        })
172    }
173}
174
175/// Load all YAML documents from a string, returning an error if parsing fails.
176///
177/// This is an alias for [`load_all`] which also returns errors on invalid
178/// syntax.
179///
180/// # Examples
181///
182/// ```rust
183/// use noyalib::document::try_load_all;
184///
185/// let yaml = "---
186/// first: 1
187/// ---
188/// second: 2
189/// ";
190///
191/// let iter = try_load_all(yaml).unwrap();
192/// assert_eq!(iter.len(), 2);
193/// ```
194///
195/// # Errors
196///
197/// Returns an error if the YAML syntax is invalid.
198pub fn try_load_all(input: &str) -> Result<DocumentIterator> {
199    load_all(input)
200}
201
202/// Load all YAML documents and deserialize them into a typed vector.
203///
204/// # Examples
205///
206/// ```rust
207/// use noyalib::document::load_all_as;
208/// use serde::Deserialize;
209///
210/// #[derive(Debug, Deserialize, PartialEq)]
211/// struct Doc {
212///     name: String,
213/// }
214///
215/// let yaml = "---
216/// name: first
217/// ---
218/// name: second
219/// ";
220///
221/// let docs: Vec<Doc> = load_all_as(yaml).unwrap();
222/// assert_eq!(docs.len(), 2);
223/// assert_eq!(docs[0].name, "first");
224/// assert_eq!(docs[1].name, "second");
225/// ```
226///
227/// # Errors
228///
229/// Returns an error if parsing fails or if any document cannot be
230/// deserialized into the target type.
231pub fn load_all_as<T>(input: &str) -> Result<Vec<T>>
232where
233    T: for<'de> serde::Deserialize<'de> + 'static,
234{
235    let parse_config = parser::ParseConfig::from(&ParserConfig::default());
236
237    #[cfg(feature = "std")]
238    {
239        let pairs = parser::parse(input, &parse_config)?;
240        let mut results = Vec::with_capacity(pairs.len());
241        let source: Arc<str> = input.into();
242
243        for (value, span_tree) in &pairs {
244            let spans = span_context::build_span_map(value, span_tree);
245            let ctx = span_context::SpanContext {
246                spans,
247                source: source.clone(),
248            };
249            let _guard = span_context::set_span_context(ctx);
250            let typed: T = crate::from_value(value)?;
251            results.push(typed);
252        }
253
254        Ok(results)
255    }
256
257    #[cfg(not(feature = "std"))]
258    {
259        let docs = parser::parse_all_values(input, &parse_config)?;
260        let mut results = Vec::with_capacity(docs.len());
261        for value in &docs {
262            let typed: T = crate::from_value(value)?;
263            results.push(typed);
264        }
265        Ok(results)
266    }
267}
268
269/// Lazy iterator that yields `Result<T>` per YAML document parsed
270/// from a reader.
271///
272/// Created by [`read`] / [`read_with_config`]. Deserialisation
273/// errors on individual documents are surfaced as `Err` values; the
274/// iterator continues so callers can recover and process subsequent
275/// documents. Syntax errors during the initial parse are returned
276/// from [`read`] / [`read_with_config`] before iteration starts.
277///
278/// # Memory
279///
280/// Today the reader is fully drained into a `String` before the
281/// underlying parser runs, so memory is `O(input_len)`. True
282/// `O(1)`-document streaming requires a parser-level rewrite that
283/// can accept incremental byte chunks; that work is tracked
284/// separately.
285#[cfg(feature = "std")]
286#[derive(Debug)]
287pub struct DocumentReadIterator<T> {
288    docs: IntoIter<Value>,
289    _phantom: PhantomData<fn() -> T>,
290}
291
292#[cfg(feature = "std")]
293impl<T> DocumentReadIterator<T> {
294    /// Total number of documents pending iteration.
295    ///
296    /// # Examples
297    ///
298    /// ```
299    /// use std::io::Cursor;
300    /// let yaml = "a: 1\n---\nb: 2\n";
301    /// let iter: noyalib::DocumentReadIterator<noyalib::Value> =
302    ///     noyalib::read(Cursor::new(yaml)).unwrap();
303    /// assert_eq!(iter.len(), 2);
304    /// ```
305    #[must_use]
306    pub fn len(&self) -> usize {
307        self.docs.len()
308    }
309
310    /// Whether the iterator has no further documents.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use std::io::Cursor;
316    /// let iter: noyalib::DocumentReadIterator<noyalib::Value> =
317    ///     noyalib::read(Cursor::new("")).unwrap();
318    /// assert!(iter.is_empty());
319    /// ```
320    #[must_use]
321    pub fn is_empty(&self) -> bool {
322        self.docs.len() == 0
323    }
324}
325
326#[cfg(feature = "std")]
327impl<T> Iterator for DocumentReadIterator<T>
328where
329    T: for<'de> serde::Deserialize<'de> + 'static,
330{
331    type Item = Result<T>;
332    fn next(&mut self) -> Option<Self::Item> {
333        let value = self.docs.next()?;
334        Some(crate::from_value(&value))
335    }
336    fn size_hint(&self) -> (usize, Option<usize>) {
337        self.docs.size_hint()
338    }
339}
340
341#[cfg(feature = "std")]
342impl<T> ExactSizeIterator for DocumentReadIterator<T> where
343    T: for<'de> serde::Deserialize<'de> + 'static
344{
345}
346
347/// Stream-decode every YAML document from a reader into typed
348/// values, yielding one `Result<T>` per document.
349///
350/// The reader is drained eagerly (see [`DocumentReadIterator`] for
351/// the memory caveat); document-by-document deserialisation is then
352/// produced lazily on demand. Per-document deserialisation errors
353/// surface as `Err` values inside the iterator so callers can
354/// recover and continue. A syntax error in the underlying YAML is
355/// returned synchronously from this function before any iteration
356/// happens.
357///
358/// # Errors
359///
360/// Returns an error if the reader fails, the YAML cannot be parsed,
361/// or any document exceeds the default security limits. Per-document
362/// deserialisation errors are *not* surfaced here; they appear
363/// inside the iterator.
364///
365/// # Examples
366///
367/// ```
368/// use std::io::Cursor;
369/// use serde::Deserialize;
370///
371/// #[derive(Debug, Deserialize, PartialEq)]
372/// struct Doc { id: u32 }
373///
374/// let yaml = "id: 1\n---\nid: 2\n---\nid: 3\n";
375/// let docs: Vec<Doc> = noyalib::read::<_, Doc>(Cursor::new(yaml))
376///     .unwrap()
377///     .filter_map(Result::ok)
378///     .collect();
379/// assert_eq!(docs, vec![Doc { id: 1 }, Doc { id: 2 }, Doc { id: 3 }]);
380/// ```
381#[cfg(feature = "std")]
382pub fn read<R, T>(reader: R) -> Result<DocumentReadIterator<T>>
383where
384    R: std::io::Read,
385    T: for<'de> serde::Deserialize<'de> + 'static,
386{
387    read_with_config(reader, &ParserConfig::default())
388}
389
390/// [`read`] with a custom [`ParserConfig`] for tightened security
391/// limits.
392///
393/// # Errors
394///
395/// Same as [`read`].
396///
397/// # Examples
398///
399/// ```
400/// use std::io::Cursor;
401/// use noyalib::{read_with_config, ParserConfig, Value};
402///
403/// let cfg = ParserConfig::strict();
404/// let yaml = "a: 1\n---\nb: 2\n";
405/// let count = read_with_config::<_, Value>(Cursor::new(yaml), &cfg)
406///     .unwrap()
407///     .count();
408/// assert_eq!(count, 2);
409/// ```
410#[cfg(feature = "std")]
411pub fn read_with_config<R, T>(
412    mut reader: R,
413    config: &ParserConfig,
414) -> Result<DocumentReadIterator<T>>
415where
416    R: std::io::Read,
417    T: for<'de> serde::Deserialize<'de> + 'static,
418{
419    let mut buf = String::new();
420    let _read_bytes = reader
421        .read_to_string(&mut buf)
422        .map_err(|e| Error::Parse(format!("reader I/O failed: {e}")))?;
423    if buf.len() > config.max_document_length.saturating_mul(64) {
424        // Soft cap on the *aggregated* multi-document buffer to
425        // bound memory regardless of per-document caps.
426        return Err(Error::Parse(format!(
427            "reader payload exceeds 64× max_document_length ({} bytes)",
428            config.max_document_length
429        )));
430    }
431    let parse_config = parser::ParseConfig::from(config);
432    let pairs = parser::parse(&buf, &parse_config)?;
433    let docs: Vec<Value> = pairs.into_iter().map(|(value, _)| value).collect();
434    Ok(DocumentReadIterator {
435        docs: docs.into_iter(),
436        _phantom: PhantomData,
437    })
438}