serde_saphyr/de/api.rs
1use std::io::Read;
2
3use serde_core::de::DeserializeOwned;
4
5use super::with_deserializer::deserialize_document_unless_null;
6use super::{Error, Events, Options, ring_reader};
7use crate::budget::EnforcingPolicy;
8use crate::live_events::LiveEvents;
9
10#[cfg(all(feature = "deserialize", feature = "include"))]
11pub(crate) fn resolver_from_options<'a>(
12 options: &Options,
13) -> Option<Box<crate::input_source::IncludeResolver<'a>>> {
14 options.include_resolver.clone().map(|rc_refcell| {
15 Box::new(move |req: crate::input_source::IncludeRequest<'_>| rc_refcell.borrow_mut()(req))
16 as Box<crate::input_source::IncludeResolver<'a>>
17 })
18}
19
20/// Deserialize any `T: serde::de::Deserialize<'de>` directly from a YAML string.
21///
22/// This is the simplest entry point; it parses a single YAML document. If the
23/// input contains multiple documents, this returns an error advising to use
24/// [`from_str_multiple`] or [`from_str_multiple_with_options`].
25///
26/// This function supports both owned types (like `String`) and borrowed types
27/// (like `&str`). For borrowed types, the deserialized value's lifetime is tied
28/// to the input string's lifetime.
29///
30/// **Note**: Borrowing only works for simple plain scalars that don't require
31/// any transformation (no multi-line folding, no escape processing). For
32/// transformed strings, deserialization to `&str` will fail with a helpful
33/// error message suggesting to use `String` or `Cow<str>` instead.
34///
35/// Example: read a small `Config` structure from a YAML string.
36///
37/// ```rust
38/// use serde::Deserialize;
39///
40/// #[derive(Debug, Deserialize, PartialEq)]
41/// struct Config {
42/// name: String,
43/// enabled: bool,
44/// retries: i32,
45/// }
46///
47/// let yaml = r#"
48/// name: My Application
49/// enabled: true
50/// retries: 5
51/// "#;
52///
53/// let cfg: Config = serde_saphyr::from_str(yaml).unwrap();
54/// assert!(cfg.enabled);
55/// ```
56///
57/// Example: read a structure with borrowed string fields.
58///
59/// Borrowed strings are supported when deserializing from an in-memory input (`from_str` / `from_slice`),
60/// and only when the scalar exists verbatim in the input (i.e., no escape processing, folding, or other
61/// normalization is required). If the YAML scalar requires transformation, deserializing into `&str`
62/// fails with an error suggesting `String` or `Cow<str>`.
63///
64/// Note: reader-based entry points like [`from_reader`] require `DeserializeOwned` and therefore cannot
65/// return values that borrow from the input.
66///
67/// ```rust
68/// use serde::Deserialize;
69///
70/// #[derive(Debug, Deserialize, PartialEq)]
71/// struct Data<'a> {
72/// name: &'a str,
73/// value: i32,
74/// }
75///
76/// let yaml = "name: hello\nvalue: 42\n";
77///
78/// let data: Data = serde_saphyr::from_str(yaml).unwrap();
79/// assert_eq!(data.name, "hello");
80/// assert_eq!(data.value, 42);
81/// ```
82#[cfg(feature = "deserialize")]
83pub fn from_str<'de, T>(input: &'de str) -> Result<T, Error>
84where
85 T: serde_core::de::Deserialize<'de>,
86{
87 from_str_with_options(input, Options::default())
88}
89
90#[cfg(feature = "deserialize")]
91fn from_str_with_options_impl<'de, T>(input: &'de str, options: Options) -> Result<T, Error>
92where
93 T: serde_core::de::Deserialize<'de>,
94{
95 super::with_deserializer::with_deserializer_from_str_with_options(input, options, |de| {
96 T::deserialize(de)
97 })
98}
99
100/// Deserialize a single YAML document with configurable [`Options`].
101///
102/// This function supports both owned types (like `String`) and borrowed types
103/// (like `&str`). For borrowed types, the deserialized value's lifetime is tied
104/// to the input string's lifetime.
105///
106/// Example: read a small `Config` with a custom budget and default duplicate-key policy.
107///
108/// ```rust
109/// use serde::Deserialize;
110/// use serde_saphyr::DuplicateKeyPolicy;
111///
112/// #[derive(Debug, Deserialize, PartialEq)]
113/// struct Config {
114/// name: String,
115/// enabled: bool,
116/// retries: i32,
117/// }
118///
119/// let yaml = r#"
120/// name: My Application
121/// enabled: true
122/// retries: 5
123/// "#;
124///
125/// let options = serde_saphyr::options! {
126/// budget: serde_saphyr::budget! {
127/// max_anchors: 200,
128/// },
129/// duplicate_keys: DuplicateKeyPolicy::FirstWins,
130/// };
131/// let cfg: Config = serde_saphyr::from_str_with_options(yaml, options).unwrap();
132/// assert_eq!(cfg.retries, 5);
133/// ```
134#[cfg(feature = "deserialize")]
135pub fn from_str_with_options<'de, T>(input: &'de str, options: Options) -> Result<T, Error>
136where
137 T: serde_core::de::Deserialize<'de>,
138{
139 from_str_with_options_impl(input, options)
140}
141
142#[cfg(feature = "deserialize")]
143pub(crate) fn maybe_with_snippet(
144 err: Error,
145 input: &str,
146 with_snippet: bool,
147 crop_radius: usize,
148) -> Error {
149 if !(with_snippet && crop_radius > 0 && err.location().is_some()) {
150 return err;
151 }
152
153 err.with_snippet(input, crop_radius)
154}
155
156#[cfg(feature = "deserialize")]
157pub(crate) struct RootFragment<'a> {
158 pub text: &'a str,
159 pub start_line: usize,
160 pub source_name: &'a str,
161}
162
163#[cfg(feature = "deserialize")]
164pub(crate) struct StrSnippetContext<'a> {
165 input: &'a str,
166 with_snippet: bool,
167 crop_radius: usize,
168}
169
170#[cfg(feature = "deserialize")]
171impl<'a> StrSnippetContext<'a> {
172 pub(crate) fn new(input: &'a str, with_snippet: bool, crop_radius: usize) -> Self {
173 Self {
174 input,
175 with_snippet,
176 crop_radius,
177 }
178 }
179
180 pub(crate) fn attach_snippet(&self, err: Error, src: &LiveEvents<'_>) -> Error {
181 maybe_with_snippet_from_events(err, self.input, src, self.with_snippet, self.crop_radius)
182 }
183}
184
185#[cfg(feature = "deserialize")]
186pub(crate) struct ReaderSnippetContext<R> {
187 shared_ring: ring_reader::SharedRingReader<R>,
188 with_snippet: bool,
189 crop_radius: usize,
190}
191
192#[cfg(feature = "deserialize")]
193impl<R: Read> ReaderSnippetContext<R> {
194 pub(crate) fn new(
195 reader: R,
196 with_snippet: bool,
197 crop_radius: usize,
198 ) -> (Self, ring_reader::SharedRingReaderHandle<R>) {
199 let shared_ring = ring_reader::SharedRingReader::new(reader);
200 let ring_handle = ring_reader::SharedRingReaderHandle::new(&shared_ring);
201 (
202 Self {
203 shared_ring,
204 with_snippet,
205 crop_radius,
206 },
207 ring_handle,
208 )
209 }
210
211 pub(crate) fn attach_snippet(&self, err: Error, src: &LiveEvents<'_>) -> Error {
212 if !self.with_snippet || self.crop_radius == 0 {
213 return err;
214 }
215
216 match self.shared_ring.get_recent() {
217 Ok(snapshot) => {
218 let text = String::from_utf8_lossy(&snapshot.bytes);
219 let root = RootFragment {
220 text: text.as_ref(),
221 start_line: snapshot.start_line,
222 source_name: "input",
223 };
224 maybe_with_snippet_from_events_and_root_fragment(
225 err,
226 Some(&root),
227 text.as_ref(),
228 src,
229 self.with_snippet,
230 self.crop_radius,
231 )
232 }
233 Err(_) => err,
234 }
235 }
236}
237
238#[cfg(all(feature = "deserialize", feature = "include"))]
239fn with_root_additional_snippet(
240 err: Error,
241 root: Option<&RootFragment<'_>>,
242 input: &str,
243 location: &crate::Location,
244 crop_radius: usize,
245) -> Error {
246 match root {
247 Some(root) => err.with_additional_snippet_offset_named(
248 root.text,
249 root.start_line,
250 root.source_name,
251 location,
252 crop_radius,
253 ),
254 None => err.with_additional_snippet_named(input, "input", location, crop_radius),
255 }
256}
257
258#[cfg(all(feature = "deserialize", feature = "include"))]
259fn with_recorded_source_snippets(
260 err: Error,
261 root: Option<&RootFragment<'_>>,
262 input: &str,
263 chain: &[&crate::include_stack::RecordedSource],
264 crop_radius: usize,
265) -> Error {
266 let Some(current) = chain.first() else {
267 return with_root_or_input_snippet(err, root, input, crop_radius);
268 };
269 let Some(source_text) = current.text.as_deref() else {
270 return with_root_or_input_snippet(err, root, input, crop_radius);
271 };
272 let err_with_snippet = err.with_snippet_named(source_text, current.name.as_str(), crop_radius);
273 with_include_site_snippets(err_with_snippet, root, input, chain, crop_radius)
274}
275
276/// Add an "included from here" window for each include site in `chain` that has source text.
277#[cfg(all(feature = "deserialize", feature = "include"))]
278fn with_include_site_snippets(
279 mut err: Error,
280 root: Option<&RootFragment<'_>>,
281 input: &str,
282 chain: &[&crate::include_stack::RecordedSource],
283 crop_radius: usize,
284) -> Error {
285 for window in chain.windows(2) {
286 let (child, parent) = (window[0], window[1]);
287 if child.include_location == crate::Location::UNKNOWN {
288 continue;
289 }
290 match parent.text.as_deref() {
291 Some(parent_text) => {
292 err = err.with_additional_snippet_named(
293 parent_text,
294 parent.name.as_str(),
295 &child.include_location,
296 crop_radius,
297 );
298 }
299 None if parent.parent_source_id.is_none() => {
300 err = with_root_additional_snippet(
301 err,
302 root,
303 input,
304 &child.include_location,
305 crop_radius,
306 );
307 }
308 None => {}
309 }
310 }
311 err
312}
313
314#[cfg(all(feature = "deserialize", feature = "include"))]
315fn with_root_or_input_snippet(
316 err: Error,
317 root: Option<&RootFragment<'_>>,
318 input: &str,
319 crop_radius: usize,
320) -> Error {
321 match root {
322 Some(root) => {
323 err.with_snippet_offset_named(root.text, root.start_line, root.source_name, crop_radius)
324 }
325 None => maybe_with_snippet(err, input, true, crop_radius),
326 }
327}
328
329#[cfg(feature = "deserialize")]
330pub(crate) fn maybe_with_snippet_from_events_and_root_fragment(
331 err: Error,
332 root: Option<&RootFragment<'_>>,
333 input: &str,
334 #[allow(unused_variables)] events: &crate::live_events::LiveEvents<'_>,
335 with_snippet: bool,
336 crop_radius: usize,
337) -> Error {
338 if !(with_snippet && crop_radius > 0 && err.location().is_some()) {
339 return err;
340 }
341
342 #[cfg(feature = "include")]
343 if let Some(loc) = err.location() {
344 let chain = events.recorded_source_chain(loc.source_id());
345 if let Some(current) = chain.first() {
346 if current.text.is_some() {
347 return with_recorded_source_snippets(err, root, input, &chain, crop_radius);
348 }
349 // An error inside an include whose text was not retained (a reader-backed include)
350 // has no source text to show. Its line/column belong to the included file, so
351 // rendering them against the root text would point at unrelated root lines.
352 // Show where it was included.
353 if current.parent_source_id.is_some() {
354 let err = err.with_no_snippet_regions(crop_radius);
355 return with_include_site_snippets(err, root, input, &chain, crop_radius);
356 }
357 }
358 }
359
360 match root {
361 Some(root) => {
362 err.with_snippet_offset_named(root.text, root.start_line, root.source_name, crop_radius)
363 }
364 None => maybe_with_snippet(err, input, with_snippet, crop_radius),
365 }
366}
367
368#[cfg(feature = "deserialize")]
369pub(crate) fn maybe_with_snippet_from_events(
370 err: Error,
371 input: &str,
372 #[allow(unused_variables)] events: &crate::live_events::LiveEvents<'_>,
373 with_snippet: bool,
374 crop_radius: usize,
375) -> Error {
376 maybe_with_snippet_from_events_and_root_fragment(
377 err,
378 None,
379 input,
380 events,
381 with_snippet,
382 crop_radius,
383 )
384}
385
386/// Deserialize multiple YAML documents from a single string into a vector of `T`.
387/// Completely empty documents, untagged plain null-like documents, and explicit `!!null`
388/// documents are skipped. Application-tagged roots are deserialized, even with null payloads.
389///
390/// Supports both owned values and values borrowing from the input string. Borrowed values
391/// cannot outlive the input, and strings requiring transformation cannot be borrowed as `&str`;
392/// see [`from_str_multiple`] for details and for collecting into other collection types.
393///
394/// Since 2.0, function pointers or callbacks accepting any input lifetime may need a
395/// forwarding closure such as `|input| from_multiple::<String>(input)`.
396///
397/// Example: read two `Config` documents separated by `---`.
398///
399/// ```rust
400/// use serde::Deserialize;
401///
402/// #[derive(Debug, Deserialize, PartialEq)]
403/// struct Config {
404/// name: String,
405/// enabled: bool,
406/// retries: i32,
407/// }
408///
409/// let yaml = r#"
410/// name: First
411/// enabled: true
412/// retries: 1
413/// ---
414/// name: Second
415/// enabled: false
416/// retries: 2
417/// "#;
418///
419/// let cfgs: Vec<Config> = serde_saphyr::from_multiple(yaml).unwrap();
420/// assert_eq!(cfgs.len(), 2);
421/// assert_eq!(cfgs[0].name, "First");
422/// ```
423#[cfg(feature = "deserialize")]
424pub fn from_multiple<'de, T>(input: &'de str) -> Result<Vec<T>, Error>
425where
426 T: serde_core::Deserialize<'de>,
427{
428 from_str_multiple::<T, Vec<T>>(input)
429}
430
431/// Deserialize multiple YAML documents into a vector with configurable [`Options`].
432///
433/// Like [`from_multiple`], this supports owned and borrowed values and skips empty,
434/// untagged null-like, and explicit `!!null` documents. With `DockerComposeWithNulls`,
435/// interpolated nulls are also skipped. Application-tagged roots are deserialized.
436/// Use [`from_str_multiple_with_options`] to collect into other collection types.
437///
438/// Since 2.0, function pointers or callbacks accepting any input lifetime may need a
439/// forwarding closure such as `|input, options| from_multiple_with_options::<String>(input, options)`.
440///
441/// Example: two `Config` documents with a custom budget.
442///
443/// ```rust
444/// use serde::Deserialize;
445/// use serde_saphyr::DuplicateKeyPolicy;
446///
447/// #[derive(Debug, Deserialize, PartialEq)]
448/// struct Config {
449/// name: String,
450/// enabled: bool,
451/// retries: i32,
452/// }
453///
454/// let yaml = r#"
455/// name: First
456/// enabled: true
457/// retries: 1
458/// ---
459/// name: Second
460/// enabled: false
461/// retries: 2
462/// "#;
463///
464/// let options = serde_saphyr::options! {
465/// budget: serde_saphyr::budget! {
466/// max_anchors: 200,
467/// },
468/// duplicate_keys: DuplicateKeyPolicy::FirstWins,
469/// };
470/// let cfgs: Vec<Config> = serde_saphyr::from_multiple_with_options(yaml, options).unwrap();
471/// assert_eq!(cfgs.len(), 2);
472/// assert!(!cfgs[1].enabled);
473/// ```
474#[cfg(feature = "deserialize")]
475pub fn from_multiple_with_options<'de, T>(
476 input: &'de str,
477 options: Options,
478) -> Result<Vec<T>, Error>
479where
480 T: serde_core::Deserialize<'de>,
481{
482 from_str_multiple_with_options::<T, Vec<T>>(input, options)
483}
484
485/// Deserialize multiple YAML documents from a string into a collection, supporting borrowed values.
486/// Empty, untagged null-like, and explicit `!!null` documents are skipped, as with
487/// [`from_multiple`]. Application-tagged roots are deserialized, even with null payloads.
488///
489/// The collection is initialized with [`Default`] and extended with each document in input
490/// order. Any `C: Default + Extend<T>` is supported, including `Vec<T>`, `VecDeque<T>`,
491/// and sets. The collection determines how ordering and duplicate values are handled.
492/// Specify both types when using turbofish syntax, for example `::<String, Vec<_>>`.
493/// If the collection implements `Extend` for multiple item types, specify `T` explicitly
494/// to resolve ambiguity, for example `::<i32, Vec<_>>`.
495/// Use [`from_multiple`] to return a `Vec<T>` with a single type argument.
496///
497/// This function supports both owned types (like `String`) and borrowed types (like `&str`).
498/// Borrowed values refer to the input string and cannot outlive it.
499///
500/// Strings can be borrowed when their parsed value exists verbatim in the input, including
501/// plain scalars and simple quoted strings. Strings requiring escape processing, folding,
502/// or other transformations cannot be deserialized into `&str`; use `String` or a
503/// `#[serde(borrow)]`-annotated `Cow<'a, str>` field to handle those values.
504///
505/// ```rust
506/// use serde::Deserialize;
507///
508/// #[derive(Debug, Deserialize)]
509/// struct Person<'a> {
510/// name: &'a str,
511/// }
512///
513/// let yaml = String::from("name: Alice\n---\nname: Bob\n");
514/// let people: Vec<Person<'_>> = serde_saphyr::from_str_multiple(&yaml)?;
515/// assert_eq!(people[0].name, "Alice");
516/// assert_eq!(people[1].name, "Bob");
517/// # Ok::<(), serde_saphyr::Error>(())
518/// ```
519#[cfg(feature = "deserialize")]
520pub fn from_str_multiple<'de, T, C>(input: &'de str) -> Result<C, Error>
521where
522 T: serde_core::Deserialize<'de>,
523 C: Default + Extend<T>,
524{
525 from_str_multiple_with_options::<T, C>(input, Options::default())
526}
527
528/// Deserialize multiple YAML documents into a collection with configurable [`Options`], supporting borrowed values.
529/// Null documents are skipped as described in [`from_multiple_with_options`].
530/// Application-tagged roots are deserialized, even with null payloads.
531///
532/// Like [`from_str_multiple`], this function supports both owned values and values borrowing
533/// from the input string, collected into any `C: Default + Extend<T>`.
534/// Borrowed values cannot outlive the input, and strings requiring
535/// transformation cannot be borrowed as `&str`.
536/// Use [`from_multiple_with_options`] to return a `Vec<T>` with a single type argument.
537///
538/// ```rust
539/// use std::collections::BTreeSet;
540///
541/// let yaml = String::from("first\n---\nsecond\n---\nfirst\n");
542/// let options = serde_saphyr::options! { with_snippet: false };
543/// let values = serde_saphyr::from_str_multiple_with_options::<&str, BTreeSet<_>>(&yaml, options)?;
544/// assert_eq!(values, BTreeSet::from(["first", "second"]));
545/// # Ok::<(), serde_saphyr::Error>(())
546/// ```
547#[cfg(feature = "deserialize")]
548pub fn from_str_multiple_with_options<'de, T, C>(
549 input: &'de str,
550 options: Options,
551) -> Result<C, Error>
552where
553 T: serde_core::Deserialize<'de>,
554 C: Default + Extend<T>,
555{
556 let snippet_ctx = StrSnippetContext::new(input, options.with_snippet, options.crop_radius);
557 let cfg = crate::de::Cfg::from_options(&options);
558 let mut src = LiveEvents::from_str(input, options);
559 let mut values = C::default();
560 let wrap_err = |e, src: &LiveEvents<'_>| snippet_ctx.attach_snippet(e, src);
561
562 loop {
563 match src.peek() {
564 Ok(Some(_)) => {
565 let value = deserialize_document_unless_null(
566 &mut src,
567 cfg,
568 |de| T::deserialize(de),
569 wrap_err,
570 )?;
571 values.extend(value);
572 }
573 Ok(None) => break,
574 Err(e) => {
575 return Err(wrap_err(e, &src));
576 }
577 }
578 }
579
580 if let Err(e) = src.finish() {
581 return Err(wrap_err(e, &src));
582 }
583 Ok(values)
584}
585
586/// Deserialize a single YAML document from a UTF-8 byte slice.
587///
588/// UTF-8 only (due borrowing). For UTF-16 input, use [`from_reader`] instead:
589/// `let reader = std::io::Cursor::new(bytes);`
590/// `let cfg: Config = serde_saphyr::from_reader(reader)?;`
591///
592/// This is equivalent to [`from_str`], but accepts `&[u8]` and validates it is
593/// valid UTF-8 before parsing.
594///
595/// This function supports both owned types (like `String`) and borrowed types
596/// (like `&str`). For borrowed types, the deserialized value's lifetime is tied
597/// to the input byte slice's lifetime.
598///
599/// Example: read a small `Config` structure from bytes.
600///
601/// ```rust
602/// use serde::Deserialize;
603///
604/// #[derive(Debug, Deserialize, PartialEq)]
605/// struct Config {
606/// name: String,
607/// enabled: bool,
608/// retries: i32,
609/// }
610///
611/// let yaml = r#"
612/// name: My Application
613/// enabled: true
614/// retries: 5
615/// "#;
616/// let bytes = yaml.as_bytes();
617/// let cfg: Config = serde_saphyr::from_slice(bytes).unwrap();
618/// assert!(cfg.enabled);
619/// ```
620///
621#[cfg(feature = "deserialize")]
622pub fn from_slice<'de, T>(bytes: &'de [u8]) -> Result<T, Error>
623where
624 T: serde_core::Deserialize<'de>,
625{
626 from_slice_with_options(bytes, Options::default())
627}
628
629/// Deserialize a single YAML document from a UTF-8 byte slice with configurable [`Options`].
630///
631/// Example: read a small `Config` with a custom budget from bytes.
632///
633/// ```rust
634/// use serde::Deserialize;
635/// use serde_saphyr::DuplicateKeyPolicy;
636///
637/// #[derive(Debug, Deserialize, PartialEq)]
638/// struct Config {
639/// name: String,
640/// enabled: bool,
641/// retries: i32,
642/// }
643///
644/// let yaml = r#"
645/// name: My Application
646/// enabled: true
647/// retries: 5
648/// "#;
649/// let bytes = yaml.as_bytes();
650/// let options = serde_saphyr::options! {
651/// budget: serde_saphyr::budget! {
652/// max_anchors: 200,
653/// },
654/// duplicate_keys: DuplicateKeyPolicy::FirstWins,
655/// };
656/// let cfg: Config = serde_saphyr::from_slice_with_options(bytes, options).unwrap();
657/// assert_eq!(cfg.retries, 5);
658/// ```
659#[cfg(feature = "deserialize")]
660pub fn from_slice_with_options<'de, T>(bytes: &'de [u8], options: Options) -> Result<T, Error>
661where
662 T: serde_core::Deserialize<'de>,
663{
664 let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
665 from_str_with_options(s, options)
666}
667
668/// Deserialize multiple YAML documents from a UTF-8 byte slice into a vector of `T`.
669/// Empty, untagged null-like, and explicit `!!null` documents are skipped, as with
670/// [`from_multiple`]. Application-tagged roots are deserialized, even with null payloads.
671///
672/// Supports both owned values and values borrowing from the input byte slice. Borrowed
673/// values cannot outlive the input, and strings requiring transformation cannot be borrowed
674/// as `&str`; see [`from_str_multiple`] for details. Invalid UTF-8 returns [`Error::InvalidUtf8Input`].
675/// Use [`from_bytes_multiple`] to collect into other collection types.
676///
677/// Since 2.0, function pointers or callbacks accepting any input lifetime may need a
678/// forwarding closure such as `|bytes| from_slice_multiple::<String>(bytes)`.
679///
680/// Example: read two `Config` documents separated by `---` from bytes.
681///
682/// ```rust
683/// use serde::Deserialize;
684///
685/// #[derive(Debug, Deserialize, PartialEq)]
686/// struct Config {
687/// name: String,
688/// enabled: bool,
689/// retries: i32,
690/// }
691///
692/// let yaml = r#"
693/// name: First
694/// enabled: true
695/// retries: 1
696/// ---
697/// name: Second
698/// enabled: false
699/// retries: 2
700/// "#;
701/// let bytes = yaml.as_bytes();
702/// let cfgs: Vec<Config> = serde_saphyr::from_slice_multiple(bytes).unwrap();
703/// assert_eq!(cfgs.len(), 2);
704/// assert_eq!(cfgs[0].name, "First");
705/// ```
706#[cfg(feature = "deserialize")]
707pub fn from_slice_multiple<'de, T>(bytes: &'de [u8]) -> Result<Vec<T>, Error>
708where
709 T: serde_core::Deserialize<'de>,
710{
711 from_bytes_multiple::<T, Vec<T>>(bytes)
712}
713
714/// Deserialize multiple YAML documents from a UTF-8 byte slice into a vector with configurable [`Options`].
715///
716/// Like [`from_multiple_with_options`], this supports owned and borrowed values and skips
717/// null documents unless application-tagged. Invalid UTF-8 returns [`Error::InvalidUtf8Input`].
718/// Use [`from_bytes_multiple_with_options`] to collect into other collection types.
719///
720/// Since 2.0, function pointers or callbacks accepting any input lifetime may need a
721/// forwarding closure such as `|bytes, options| from_slice_multiple_with_options::<String>(bytes, options)`.
722///
723/// Example: two `Config` documents with a custom budget from bytes.
724///
725/// ```rust
726/// use serde::Deserialize;
727/// use serde_saphyr::DuplicateKeyPolicy;
728///
729/// #[derive(Debug, Deserialize, PartialEq)]
730/// struct Config {
731/// name: String,
732/// enabled: bool,
733/// retries: i32,
734/// }
735///
736/// let yaml = r#"
737/// name: First
738/// enabled: true
739/// retries: 1
740/// ---
741/// name: Second
742/// enabled: false
743/// retries: 2
744/// "#;
745/// let bytes = yaml.as_bytes();
746/// let options = serde_saphyr::options! {
747/// budget: serde_saphyr::budget! {
748/// max_anchors: 200,
749/// },
750/// duplicate_keys: DuplicateKeyPolicy::FirstWins,
751/// };
752/// let cfgs: Vec<Config> = serde_saphyr::from_slice_multiple_with_options(bytes, options).unwrap();
753/// assert_eq!(cfgs.len(), 2);
754/// assert!(!cfgs[1].enabled);
755/// ```
756#[cfg(feature = "deserialize")]
757pub fn from_slice_multiple_with_options<'de, T>(
758 bytes: &'de [u8],
759 options: Options,
760) -> Result<Vec<T>, Error>
761where
762 T: serde_core::Deserialize<'de>,
763{
764 from_bytes_multiple_with_options::<T, Vec<T>>(bytes, options)
765}
766
767/// Deserialize multiple YAML documents from a UTF-8 byte slice into a collection, supporting borrowed values.
768/// Empty, untagged null-like, and explicit `!!null` documents are skipped, as with
769/// [`from_str_multiple`]. Application-tagged roots are deserialized, even with null payloads.
770///
771/// This is equivalent to [`from_str_multiple`], but validates that the bytes are UTF-8
772/// before parsing. Invalid UTF-8 returns [`Error::InvalidUtf8Input`]. For UTF-16 streams,
773/// use [`read`] or [`read_with_options`] instead.
774///
775/// Any `C: Default + Extend<T>` is supported. Documents extend the collection in input
776/// order; the collection determines how ordering and duplicate values are handled.
777/// Specify both types when using turbofish syntax, for example `::<String, Vec<_>>`.
778/// Use [`from_slice_multiple`] to return a `Vec<T>` with a single type argument.
779///
780/// This function supports both owned types (like `String`) and borrowed types (like `&str`).
781/// Borrowed values refer to the input byte slice and cannot outlive it.
782/// Strings requiring escape processing, folding, or other transformations cannot be
783/// borrowed as `&str`; see [`from_str_multiple`] for details.
784///
785/// ```rust
786/// use serde::Deserialize;
787///
788/// #[derive(Debug, Deserialize)]
789/// struct Person<'a> {
790/// name: &'a str,
791/// }
792///
793/// let bytes = b"name: Alice\n---\nname: Bob\n".to_vec();
794/// let people: Vec<Person<'_>> = serde_saphyr::from_bytes_multiple(&bytes)?;
795/// assert_eq!(people[0].name, "Alice");
796/// assert_eq!(people[1].name, "Bob");
797/// # Ok::<(), serde_saphyr::Error>(())
798/// ```
799#[cfg(feature = "deserialize")]
800pub fn from_bytes_multiple<'de, T, C>(bytes: &'de [u8]) -> Result<C, Error>
801where
802 T: serde_core::Deserialize<'de>,
803 C: Default + Extend<T>,
804{
805 from_bytes_multiple_with_options::<T, C>(bytes, Options::default())
806}
807
808/// Deserialize multiple YAML documents from UTF-8 bytes into a collection with configurable [`Options`],
809/// supporting borrowed values.
810/// Null documents are skipped as described in [`from_str_multiple_with_options`].
811/// Application-tagged roots are deserialized, even with null payloads.
812///
813/// Like [`from_bytes_multiple`], this function supports owned values and values borrowing
814/// from the input byte slice, collected into any `C: Default + Extend<T>`.
815/// Borrowed values cannot outlive the input, and strings requiring
816/// transformation cannot be borrowed as `&str`. Invalid UTF-8 returns [`Error::InvalidUtf8Input`].
817/// Use [`from_slice_multiple_with_options`] to return a `Vec<T>` with a single type argument.
818///
819/// ```rust
820/// use std::collections::VecDeque;
821///
822/// let bytes = b"first\n---\nsecond\n".to_vec();
823/// let options = serde_saphyr::options! { with_snippet: false };
824/// let values = serde_saphyr::from_bytes_multiple_with_options::<&str, VecDeque<_>>(&bytes, options)?;
825/// assert_eq!(values, VecDeque::from(["first", "second"]));
826/// # Ok::<(), serde_saphyr::Error>(())
827/// ```
828#[cfg(feature = "deserialize")]
829pub fn from_bytes_multiple_with_options<'de, T, C>(
830 bytes: &'de [u8],
831 options: Options,
832) -> Result<C, Error>
833where
834 T: serde_core::Deserialize<'de>,
835 C: Default + Extend<T>,
836{
837 let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
838 from_str_multiple_with_options::<T, C>(s, options)
839}
840
841/// Deserialize a single YAML document from any `std::io::Read`.
842///
843/// Reader-based entry points accept BOM-marked UTF-8, UTF-16LE, and UTF-16BE. If no
844/// recognized BOM is present, the input bytes are treated as UTF-8.
845///
846/// This method parses as it reads, without loading the entire input into memory first. Hence,
847/// budget limits protect against large (potentially malicious) input.
848///
849/// Example
850///
851/// ```rust
852/// use serde::{Deserialize, Serialize};
853/// use std::collections::HashMap;
854/// use serde_json::Value;
855///
856/// #[derive(Debug, PartialEq, Serialize, Deserialize)]
857/// struct Point {
858/// x: i32,
859/// y: i32,
860/// }
861///
862/// let yaml = "x: 3\ny: 4\n";
863/// let reader = std::io::Cursor::new(yaml.as_bytes());
864/// let p: Point = serde_saphyr::from_reader(reader).unwrap();
865/// assert_eq!(p, Point { x: 3, y: 4 });
866///
867/// // It also works for dynamic values like serde_json::Value
868/// let mut big = String::new();
869/// let mut i = 0usize;
870/// while big.len() < 64 * 1024 { big.push_str(&format!("k{0}: v{0}\n", i)); i += 1; }
871/// let reader = std::io::Cursor::new(big.as_bytes().to_owned());
872/// let _value: Value = serde_saphyr::from_reader(reader).unwrap();
873/// ```
874#[cfg(feature = "deserialize")]
875pub fn from_reader<'a, R: std::io::Read + 'a, T: DeserializeOwned>(reader: R) -> Result<T, Error> {
876 from_reader_with_options(reader, Options::default())
877}
878
879/// Deserialize a single YAML document from any `std::io::Read` with configurable `Options`.
880///
881/// This is the reader-based counterpart to [`from_str_with_options`]. It consumes a
882/// byte-oriented reader and streams events into the deserializer. BOM-marked
883/// UTF-8, UTF-16LE, and UTF-16BE inputs are transcoded to UTF-8 internally
884/// before parsing. If no recognized BOM is present, the input bytes are
885/// treated as UTF-8.
886///
887/// This method parses as it reads, without loading the entire input into memory first. Hence,
888/// budget limits protect against large (potentially malicious) input.
889///
890/// Notes on limits and large inputs
891/// - Parsing limits: Use [`Options::budget`] to constrain YAML complexity (events, nodes,
892/// nesting depth, total scalar bytes, total comment bytes, number of documents, anchors,
893/// aliases, etc.). These
894/// limits are enforced during parsing and are enabled by default via `Options::default()`.
895/// - Byte-level input cap: `Budget::max_reader_input_bytes` is enforced while reading.
896/// The default budget sets this to 256 MiB. You can override it by customizing `Options::budget`.
897/// When the cap is exceeded, deserialization fails early with a budget error.
898///
899/// Example: limit raw input bytes and customize options
900/// ```rust
901/// use std::io::{Read, Cursor};
902/// use serde::Deserialize;
903/// use serde_saphyr::{Budget, Options};
904///
905/// #[derive(Debug, Deserialize, PartialEq)]
906/// struct Point { x: i32, y: i32 }
907///
908/// let yaml = "x: 3\ny: 4\n";
909/// let reader = Cursor::new(yaml.as_bytes());
910///
911/// let opts = serde_saphyr::options! {
912/// budget: serde_saphyr::budget! {
913/// max_events: 10_000,
914/// max_reader_input_bytes: Some(1024),
915/// },
916/// };
917///
918/// let p: Point = serde_saphyr::from_reader_with_options(reader, opts).unwrap();
919/// assert_eq!(p, Point { x: 3, y: 4 });
920/// ```
921///
922/// Error behavior
923/// - If an empty document is provided (no content), a type-mismatch (eof) error is returned when
924/// attempting to deserialize into non-null-like targets.
925/// - If the reader contains multiple documents, an error is returned suggesting the
926/// `read`/`read_with_options` iterator APIs.
927/// - If `Options::budget` is set and a limit is exceeded, an error is returned early.
928#[cfg(feature = "deserialize")]
929pub fn from_reader_with_options<'a, R: std::io::Read + 'a, T: DeserializeOwned>(
930 reader: R,
931 options: Options,
932) -> Result<T, Error> {
933 super::with_deserializer::with_deserializer_from_reader_with_options(reader, options, |de| {
934 T::deserialize(de)
935 })
936}
937
938/// Create an iterator over YAML documents from any `std::io::Read` using default options.
939///
940/// This is a convenience wrapper around [`read_with_options`] that uses the
941/// same defaults as [`Options::default`] **except** it disables the
942/// `max_reader_input_bytes` budget to better support long-lived streams.
943///
944/// - It streams the reader without loading the whole input into memory.
945/// - Each item produced by the returned iterator is one deserialized YAML document of type `T`.
946/// - Empty, untagged null-like, and explicit `!!null` documents are skipped.
947/// Application-tagged roots are deserialized, even with null payloads.
948///
949/// Generic parameters
950/// - `R`: the concrete reader type implementing [`std::io::Read`]. You almost never need to
951/// write this explicitly; the compiler will infer it from the `reader` you pass. When using
952/// turbofish, write `_` to let the compiler infer `R`.
953/// - `T`: the type to deserialize each YAML document into. Must implement [`serde::de::DeserializeOwned`].
954///
955/// Lifetimes
956/// - `'a`: the lifetime of the returned iterator, tied to the lifetime of the provided `reader`.
957/// The iterator cannot outlive the reader it was created from.
958///
959/// Limits and budget
960/// - Uses the same limits as `Options::default()` (events, nodes, nesting depth, total scalar
961/// bytes, total comment bytes) and the default alias-replay caps. The only change is that
962/// `Budget::max_reader_input_bytes` is set to `None` so the streaming iterator can handle
963/// arbitrarily long inputs. To customize these limits, call [`read_with_options`] and set
964/// `Options::budget.max_reader_input_bytes` in the provided `Options`.
965/// - Alias replay limits are also enforced with their default values to mitigate alias bombs.
966///
967/// ```rust
968/// use serde::Deserialize;
969///
970/// #[derive(Debug, Deserialize, PartialEq)]
971/// struct Simple { id: usize }
972///
973/// let yaml = b"id: 1\n---\nid: 2\n";
974/// let mut reader = std::io::Cursor::new(&yaml[..]);
975///
976/// // Type `T` is inferred from the collection target (Vec<Simple>).
977/// let values: Vec<Simple> = serde_saphyr::read(&mut reader)
978/// .map(|r| r.unwrap())
979/// .collect();
980/// assert_eq!(values.len(), 2);
981/// assert_eq!(values[0].id, 1);
982/// ```
983///
984/// Specifying only `T` with turbofish and letting `R` be inferred using `_`:
985/// ```rust
986/// use serde::Deserialize;
987///
988/// #[derive(Debug, Deserialize, PartialEq)]
989/// struct Simple { id: usize }
990///
991/// let yaml = b"id: 10\n---\nid: 20\n";
992/// let mut reader = std::io::Cursor::new(&yaml[..]);
993///
994/// // First turbofish parameter is R (reader type), `_` lets the compiler infer it.
995/// let iter = serde_saphyr::read::<_, Simple>(&mut reader);
996/// let ids: Vec<usize> = iter.map(|res| res.unwrap().id).collect();
997/// assert_eq!(ids, vec![10, 20]);
998/// ```
999///
1000/// - Each `next()` yields either `Ok(T)` for a successfully deserialized document or `Err(Error)`
1001/// if parsing or deserialization fails.
1002/// - After a **deserialization error** (e.g., type mismatch, missing field), the iterator
1003/// skips to the next document boundary (`---`) and continues.
1004/// - After a **syntax error** or **budget/alias limit exceeded**, the iterator ends.
1005/// - Null documents without an application tag are skipped and produce no items.
1006///
1007/// *Note* Some content of the next document is read before the current parsed document is emitted.
1008/// Hence, while streaming is good for safely parsing large files with multiple documents without
1009/// loading it into RAM in advance, it does not emit each document exactly
1010/// after `---` is encountered.
1011#[cfg(feature = "deserialize")]
1012pub fn read<'a, R, T>(reader: &'a mut R) -> impl Iterator<Item = Result<T, Error>> + 'a
1013where
1014 R: Read + 'a,
1015 T: DeserializeOwned + 'a,
1016{
1017 read_with_options(
1018 reader,
1019 crate::options! {
1020 budget: crate::budget! {
1021 max_reader_input_bytes: None,
1022 },
1023 },
1024 )
1025}
1026
1027/// Create an iterator over YAML documents from any `std::io::Read`, with configurable options.
1028///
1029/// This is the multi-document counterpart to [`from_reader_with_options`]. It does not load
1030/// the entire input into memory. Instead, it streams the reader, deserializing one document
1031/// at a time into values of type `T`, yielding them through the returned iterator. Documents
1032/// that are empty, untagged null-like, or explicitly tagged `!!null` are skipped.
1033/// With `DockerComposeWithNulls`, interpolated nulls are also skipped.
1034/// Application-tagged roots are deserialized, even with null payloads.
1035/// Like [`from_reader_with_options`], BOM-marked UTF-8, UTF-16LE, and UTF-16BE
1036/// inputs are transcoded to UTF-8 internally before parsing. If no recognized
1037/// BOM is present, the input bytes are treated as UTF-8.
1038///
1039/// Generic parameters
1040/// - `R`: the concrete reader type that implements [`std::io::Read`]. You rarely need to spell
1041/// this out; it is almost always inferred from the `reader` value you pass in. When using
1042/// turbofish, you can write `_` for this parameter to let the compiler infer it.
1043/// - `T`: the type to deserialize each YAML document into. This must implement [`serde::de::DeserializeOwned`].
1044///
1045/// Lifetimes
1046/// - `'a`: the lifetime of the returned iterator. It is tied to the lifetime of the provided
1047/// `reader` value because the iterator borrows internal state that references the reader.
1048/// In practice, this means the iterator cannot outlive the reader it was created from.
1049///
1050/// Limits and budget
1051/// - All parsing limits configured via [`Options::budget`] (such as maximum events, nodes,
1052/// nesting depth, total scalar bytes, total comment bytes) are enforced while streaming. The
1053/// reader input-byte cap is also enforced via `Budget::max_reader_input_bytes` (256 MiB by
1054/// default). Set this to `None` if the stream may legitimately run without a fixed byte cap.
1055/// - Alias replay limits from [`Options::alias_limits`] are also enforced to mitigate alias bombs.
1056///
1057/// ```rust
1058/// use serde::Deserialize;
1059///
1060/// #[derive(Debug, Deserialize, PartialEq)]
1061/// struct Simple { id: usize }
1062///
1063/// let yaml = b"id: 1\n---\nid: 2\n";
1064/// let mut reader = std::io::Cursor::new(&yaml[..]);
1065///
1066/// // Type `T` is inferred from the collection target (Vec<Simple>).
1067/// let values: Vec<Simple> = serde_saphyr::read(&mut reader)
1068/// .map(|r| r.unwrap())
1069/// .collect();
1070/// assert_eq!(values.len(), 2);
1071/// assert_eq!(values[0].id, 1);
1072/// ```
1073///
1074/// Specifying only `T` with turbofish and letting `R` be inferred using `_`:
1075/// ```rust
1076/// use serde::Deserialize;
1077///
1078/// #[derive(Debug, Deserialize, PartialEq)]
1079/// struct Simple { id: usize }
1080///
1081/// let yaml = b"id: 10\n---\nid: 20\n";
1082/// let mut reader = std::io::Cursor::new(&yaml[..]);
1083///
1084/// // First turbofish parameter is R (reader type) which we let the compiler infer via `_`.
1085/// let iter = serde_saphyr::read_with_options::<_, Simple>(&mut reader, serde_saphyr::Options::default());
1086/// let ids: Vec<usize> = iter.map(|res| res.unwrap().id).collect();
1087/// assert_eq!(ids, vec![10, 20]);
1088/// ```
1089///
1090/// - Each `next()` yields either `Ok(T)` for a successfully deserialized document or `Err(Error)`
1091/// if parsing or deserialization fails.
1092/// - After a **deserialization error** (e.g., type mismatch, missing field), the iterator
1093/// automatically recovers by skipping to the next document boundary (`---`) and continues
1094/// iteration. This allows processing subsequent valid documents even when some fail.
1095/// - After a **syntax error** or **budget/alias limit exceeded**, the iterator ends because
1096/// the parser state may be unrecoverable.
1097/// - Null documents without an application tag are skipped and produce no items.
1098#[cfg(feature = "deserialize")]
1099pub fn read_with_options<'a, R, T>(
1100 reader: &'a mut R, // iterator must not outlive this borrow
1101 options: Options,
1102) -> impl Iterator<Item = Result<T, Error>> + 'a
1103where
1104 R: Read + 'a,
1105 T: DeserializeOwned + 'a,
1106{
1107 struct ReadIter<'a, T> {
1108 src: LiveEvents<'a>, // borrows from `reader`
1109 cfg: crate::de::Cfg,
1110 finished: bool,
1111 _marker: std::marker::PhantomData<T>,
1112 }
1113
1114 impl<'a, T> Iterator for ReadIter<'a, T>
1115 where
1116 T: DeserializeOwned + 'a,
1117 {
1118 type Item = Result<T, Error>;
1119
1120 fn next(&mut self) -> Option<Self::Item> {
1121 if self.finished {
1122 return None;
1123 }
1124 loop {
1125 match self.src.peek() {
1126 Ok(Some(_)) => {
1127 let res = match deserialize_document_unless_null(
1128 &mut self.src,
1129 self.cfg,
1130 |de| T::deserialize(de),
1131 |e, _| e,
1132 ) {
1133 Ok(None) => continue,
1134 Ok(Some(value)) => Ok(value),
1135 Err(e) if e.is_property_interpolation_budget_breach() => {
1136 // Property interpolation records breaches across documents;
1137 // ordinary per-document budgets keep their recovery path.
1138 self.finished = true;
1139 let _ = self.src.finish();
1140 return Some(Err(e));
1141 }
1142 // Interpolation errors recover at the next document just like
1143 // errors encountered while deserializing the document's value.
1144 Err(e) => Err(e),
1145 };
1146 if res.is_err() {
1147 // After a deserialization error, skip remaining events in the
1148 // current document and try to recover at the next document boundary.
1149 // If no next document is found, mark as finished.
1150 if !self.src.skip_to_next_document() {
1151 self.finished = true;
1152 let _ = self.src.finish();
1153 }
1154 }
1155 return Some(res);
1156 }
1157 Ok(None) => {
1158 self.finished = true;
1159 if let Err(e) = self.src.finish() {
1160 return Some(Err(e));
1161 }
1162 return None;
1163 }
1164 Err(e) => {
1165 self.finished = true;
1166 let _ = self.src.finish();
1167 return Some(Err(e));
1168 }
1169 }
1170 }
1171 }
1172 }
1173
1174 let cfg = crate::de::Cfg::from_options(&options);
1175 let src = LiveEvents::from_reader(reader, options, EnforcingPolicy::PerDocument);
1176
1177 ReadIter::<T> {
1178 src,
1179 cfg,
1180 finished: false,
1181 _marker: std::marker::PhantomData,
1182 }
1183}