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 mut pairs = parser::parse(input, &parse_config)?;
155        for (doc, _) in &mut pairs {
156            crate::de::post_process_document(doc, config)?;
157        }
158        let (docs, span_trees): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
159        let total = docs.len();
160        Ok(DocumentIterator {
161            docs: docs.into_iter(),
162            _span_trees: span_trees,
163            total,
164        })
165    }
166
167    #[cfg(not(feature = "std"))]
168    {
169        let mut docs = parser::parse_all_values(input, &parse_config)?;
170        for doc in &mut docs {
171            crate::de::post_process_document(doc, config)?;
172        }
173        let total = docs.len();
174        Ok(DocumentIterator {
175            docs: docs.into_iter(),
176            total,
177        })
178    }
179}
180
181/// Load all YAML documents from a string, returning an error if parsing fails.
182///
183/// This is an alias for [`load_all`] which also returns errors on invalid
184/// syntax.
185///
186/// # Examples
187///
188/// ```rust
189/// use noyalib::document::try_load_all;
190///
191/// let yaml = "---
192/// first: 1
193/// ---
194/// second: 2
195/// ";
196///
197/// let iter = try_load_all(yaml).unwrap();
198/// assert_eq!(iter.len(), 2);
199/// ```
200///
201/// # Errors
202///
203/// Returns an error if the YAML syntax is invalid.
204pub fn try_load_all(input: &str) -> Result<DocumentIterator> {
205    load_all(input)
206}
207
208/// Load all YAML documents and deserialize them into a typed vector.
209///
210/// # Examples
211///
212/// ```rust
213/// use noyalib::document::load_all_as;
214///
215/// #[derive(Debug, serde::Deserialize, PartialEq)]
216/// struct Doc {
217///     name: String,
218/// }
219///
220/// let yaml = "---
221/// name: first
222/// ---
223/// name: second
224/// ";
225///
226/// let docs: Vec<Doc> = load_all_as(yaml).unwrap();
227/// assert_eq!(docs.len(), 2);
228/// assert_eq!(docs[0].name, "first");
229/// assert_eq!(docs[1].name, "second");
230/// ```
231///
232/// # Errors
233///
234/// Returns an error if parsing fails or if any document cannot be
235/// deserialized into the target type.
236pub fn load_all_as<T>(input: &str) -> Result<Vec<T>>
237where
238    T: for<'de> serde_core::Deserialize<'de> + 'static,
239{
240    load_all_as_with_config(input, &ParserConfig::default())
241}
242
243/// [`load_all_as`] with a custom [`ParserConfig`]: its limits, its
244/// scalar-resolution and merge settings, and its policies.
245///
246/// # Errors
247///
248/// Returns an error if the stream exceeds a configured limit, a policy
249/// refuses a document, parsing fails, or a document cannot be
250/// deserialized into `T`.
251///
252/// # Examples
253///
254/// ```
255/// use noyalib::{load_all_as_with_config, ParserConfig};
256/// let cfg = ParserConfig::strict();
257/// let docs: Vec<i32> = load_all_as_with_config("1\n---\n2\n", &cfg).unwrap();
258/// assert_eq!(docs, vec![1, 2]);
259/// ```
260pub fn load_all_as_with_config<T>(input: &str, config: &ParserConfig) -> Result<Vec<T>>
261where
262    T: for<'de> serde_core::Deserialize<'de> + 'static,
263{
264    if input.len() > config.max_document_length {
265        return Err(Error::Parse(format!(
266            "document exceeds maximum length of {} bytes",
267            config.max_document_length
268        )));
269    }
270    let parse_config = parser::ParseConfig::from(config);
271
272    #[cfg(feature = "std")]
273    {
274        let mut pairs = parser::parse(input, &parse_config)?;
275        let mut results = Vec::with_capacity(pairs.len());
276        let source: Arc<str> = input.into();
277        let lines = span_context::SharedLineIndex::default();
278
279        for (value, span_tree) in &mut pairs {
280            crate::de::post_process_document(value, config)?;
281            let spans = span_context::build_span_map(value, span_tree);
282            let ctx = span_context::SpanContext::with_lines(spans, source.clone(), lines.clone())
283                .with_key_texts(value, span_tree);
284            let typed: T = crate::de::from_value_spanned_with_options(
285                value,
286                ctx,
287                config.ignore_binary_tag_for_string,
288                config.plain_scalar_strings,
289            )?;
290            results.push(typed);
291        }
292
293        Ok(results)
294    }
295
296    #[cfg(not(feature = "std"))]
297    {
298        let mut docs = parser::parse_all_values(input, &parse_config)?;
299        let mut results = Vec::with_capacity(docs.len());
300        for value in &mut docs {
301            crate::de::post_process_document(value, config)?;
302            let typed: T = crate::de::from_value_with_options(
303                value,
304                config.ignore_binary_tag_for_string,
305                config.plain_scalar_strings,
306            )?;
307            results.push(typed);
308        }
309        Ok(results)
310    }
311}
312
313/// Lazy iterator that yields `Result<T>` per YAML document parsed
314/// from a reader.
315///
316/// Created by [`read`] / [`read_with_config`]. Deserialisation
317/// errors on individual documents are surfaced as `Err` values; the
318/// iterator continues so callers can recover and process subsequent
319/// documents. Syntax errors during the initial parse are returned
320/// from [`read`] / [`read_with_config`] before iteration starts.
321///
322/// # Memory
323///
324/// Today the reader is fully drained into a `String` before the
325/// underlying parser runs, so memory is `O(input_len)`. True
326/// `O(1)`-document streaming requires a parser-level rewrite that
327/// can accept incremental byte chunks; that work is tracked
328/// separately.
329#[cfg(feature = "std")]
330#[derive(Debug)]
331pub struct DocumentReadIterator<T> {
332    docs: IntoIter<(Value, SpanTree)>,
333    source: Arc<str>,
334    ignore_binary_tag_for_string: bool,
335    plain_scalar_strings: bool,
336    _phantom: PhantomData<fn() -> T>,
337}
338
339#[cfg(feature = "std")]
340impl<T> DocumentReadIterator<T> {
341    /// Total number of documents pending iteration.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use std::io::Cursor;
347    /// let yaml = "a: 1\n---\nb: 2\n";
348    /// let iter: noyalib::DocumentReadIterator<noyalib::Value> =
349    ///     noyalib::read(Cursor::new(yaml)).unwrap();
350    /// assert_eq!(iter.len(), 2);
351    /// ```
352    #[must_use]
353    pub fn len(&self) -> usize {
354        self.docs.len()
355    }
356
357    /// Whether the iterator has no further documents.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use std::io::Cursor;
363    /// let iter: noyalib::DocumentReadIterator<noyalib::Value> =
364    ///     noyalib::read(Cursor::new("")).unwrap();
365    /// assert!(iter.is_empty());
366    /// ```
367    #[must_use]
368    pub fn is_empty(&self) -> bool {
369        self.docs.len() == 0
370    }
371}
372
373#[cfg(feature = "std")]
374impl<T> Iterator for DocumentReadIterator<T>
375where
376    T: for<'de> serde_core::Deserialize<'de> + 'static,
377{
378    type Item = Result<T>;
379    fn next(&mut self) -> Option<Self::Item> {
380        let (value, span_tree) = self.docs.next()?;
381        let spans = span_context::build_span_map(&value, &span_tree);
382        let ctx = span_context::SpanContext::new(spans, self.source.clone())
383            .with_key_texts(&value, &span_tree);
384        Some(crate::de::from_value_spanned_with_options(
385            &value,
386            ctx,
387            self.ignore_binary_tag_for_string,
388            self.plain_scalar_strings,
389        ))
390    }
391    fn size_hint(&self) -> (usize, Option<usize>) {
392        self.docs.size_hint()
393    }
394}
395
396#[cfg(feature = "std")]
397impl<T> ExactSizeIterator for DocumentReadIterator<T> where
398    T: for<'de> serde_core::Deserialize<'de> + 'static
399{
400}
401
402/// Stream-decode every YAML document from a reader into typed
403/// values, yielding one `Result<T>` per document.
404///
405/// The reader is drained eagerly (see [`DocumentReadIterator`] for
406/// the memory caveat); document-by-document deserialisation is then
407/// produced lazily on demand. Per-document deserialisation errors
408/// surface as `Err` values inside the iterator so callers can
409/// recover and continue. A syntax error in the underlying YAML is
410/// returned synchronously from this function before any iteration
411/// happens.
412///
413/// # Errors
414///
415/// Returns an error if the reader fails, the YAML cannot be parsed,
416/// or any document exceeds the default security limits. Per-document
417/// deserialisation errors are *not* surfaced here; they appear
418/// inside the iterator.
419///
420/// # Examples
421///
422/// ```
423/// use std::io::Cursor;
424///
425/// #[derive(Debug, serde::Deserialize, PartialEq)]
426/// struct Doc { id: u32 }
427///
428/// let yaml = "id: 1\n---\nid: 2\n---\nid: 3\n";
429/// let docs: Vec<Doc> = noyalib::read::<_, Doc>(Cursor::new(yaml))
430///     .unwrap()
431///     .filter_map(Result::ok)
432///     .collect();
433/// assert_eq!(docs, vec![Doc { id: 1 }, Doc { id: 2 }, Doc { id: 3 }]);
434/// ```
435#[cfg(feature = "std")]
436pub fn read<R, T>(reader: R) -> Result<DocumentReadIterator<T>>
437where
438    R: std::io::Read,
439    T: for<'de> serde_core::Deserialize<'de> + 'static,
440{
441    read_with_config(reader, &ParserConfig::default())
442}
443
444/// [`read`] with a custom [`ParserConfig`] for tightened security
445/// limits.
446///
447/// # Errors
448///
449/// Same as [`read`].
450///
451/// # Examples
452///
453/// ```
454/// use std::io::Cursor;
455/// use noyalib::{read_with_config, ParserConfig, Value};
456///
457/// let cfg = ParserConfig::strict();
458/// let yaml = "a: 1\n---\nb: 2\n";
459/// let count = read_with_config::<_, Value>(Cursor::new(yaml), &cfg)
460///     .unwrap()
461///     .count();
462/// assert_eq!(count, 2);
463/// ```
464#[cfg(feature = "std")]
465pub fn read_with_config<R, T>(
466    mut reader: R,
467    config: &ParserConfig,
468) -> Result<DocumentReadIterator<T>>
469where
470    R: std::io::Read,
471    T: for<'de> serde_core::Deserialize<'de> + 'static,
472{
473    use std::io::Read as _;
474
475    // Cap on the *aggregated* multi-document buffer: `max_stream_bytes`,
476    // or 64 times `max_document_length` when that is smaller. Reading
477    // stops one byte past it, so an unbounded reader is refused
478    // without being buffered first.
479    let doc_cap = config.max_document_length.saturating_mul(64);
480    let cap = config.max_stream_bytes.min(doc_cap);
481    let limit = u64::try_from(cap).unwrap_or(u64::MAX).saturating_add(1);
482    let mut bytes = Vec::new();
483    let _read_bytes = reader
484        .by_ref()
485        .take(limit)
486        .read_to_end(&mut bytes)
487        .map_err(|e| Error::Parse(format!("reader I/O failed: {e}")))?;
488    if bytes.len() > cap {
489        return Err(Error::Parse(if cap < doc_cap {
490            format!("reader payload exceeds max_stream_bytes ({cap} bytes)")
491        } else {
492            format!(
493                "reader payload exceeds 64× max_document_length ({} bytes)",
494                config.max_document_length
495            )
496        }));
497    }
498    let buf = String::from_utf8(bytes).map_err(|_| {
499        Error::Parse("reader I/O failed: stream did not contain valid UTF-8".into())
500    })?;
501    // The stream may hold up to 64 documents' worth of bytes, but each
502    // document is still held to `max_document_length`, as every other
503    // entry point holds it.
504    for doc in crate::doc_boundary::split_documents_checked(&buf, config.max_documents)? {
505        if doc.len() > config.max_document_length {
506            return Err(Error::Parse(format!(
507                "document exceeds maximum length of {} bytes",
508                config.max_document_length
509            )));
510        }
511    }
512    let parse_config = parser::ParseConfig::from(config);
513    let mut pairs = parser::parse(&buf, &parse_config)?;
514    for (doc, _) in &mut pairs {
515        crate::de::post_process_document(doc, config)?;
516    }
517    Ok(DocumentReadIterator {
518        docs: pairs.into_iter(),
519        source: buf.into(),
520        ignore_binary_tag_for_string: config.ignore_binary_tag_for_string,
521        plain_scalar_strings: config.plain_scalar_strings,
522        _phantom: PhantomData,
523    })
524}