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}