#![forbid(unsafe_code)]
// Options structs expose their fields for now, but callers are expected to migrate to the
// `options!` / `ser_options!` macros. The fields are deprecated to guide downstream
// users, while this crate still legitimately reads them internally.
#![allow(deprecated)]
//! Serialization public API is defined at crate root

pub use anchors::{
    ArcAnchor, ArcRecursion, ArcRecursive, ArcWeakAnchor, RcAnchor, RcRecursion, RcRecursive,
    RcWeakAnchor,
};
pub use de::{Budget, DuplicateKeyPolicy, Error, Options};
pub use de_error::CroppedRegion;
pub use de_error::TransformReason;
pub use de_error::{MessageFormatter, RenderOptions, SnippetMode, UserMessageFormatter};
pub use indentation::RequireIndent;
pub use input_source::{
    IncludeRequest, IncludeResolveError, IncludeResolver, InputSource, ResolveProblem,
    ResolvedInclude,
};
pub use localizer::{
    DEFAULT_ENGLISH_LOCALIZER, DefaultEnglishLocalizer, ExternalMessage, ExternalMessageSource,
    Localizer,
};
pub use location::{Location, Locations, Span};
pub use long_strings::{FoldStr, FoldString, LitStr, LitString};
pub use message_formatters::{DefaultMessageFormatter, DeveloperMessageFormatter};
#[cfg(feature = "include_fs")]
pub use safe_resolver::{SafeFileReadMode, SafeFileResolver, SymlinkPolicy};
pub use ser::{Commented, FlowMap, FlowSeq, SpaceAfter};
pub use spanned::Spanned;

#[cfg(feature = "include")]
pub(crate) fn resolver_from_options<'a>(
    options: Options,
) -> Option<Box<crate::input_source::IncludeResolver<'a>>> {
    options.include_resolver.clone().map(|rc_refcell| {
        Box::new(move |req: crate::input_source::IncludeRequest<'_>| rc_refcell.borrow_mut()(req))
            as Box<crate::input_source::IncludeResolver<'a>>
    })
}

use crate::budget::EnforcingPolicy;
use crate::de::{Ev, Events};
#[cfg(feature = "garde")]
use crate::de_error::collect_garde_issues;
#[cfg(feature = "validator")]
use crate::de_error::collect_validator_issues;
#[cfg(any(feature = "garde", feature = "validator"))]
use crate::de_error::redact_issue;
use crate::live_events::LiveEvents;
use crate::parse_scalars::scalar_is_nullish;
use crate::properties_redaction::with_interp_redaction_scope;
pub use crate::serializer_options::SerializerOptions;
use serde::de::DeserializeOwned;
use std::io::Read;

#[cfg(feature = "garde")]
use garde::Validate;
#[cfg(feature = "validator")]
use validator::Validate as ValidatorValidate;

#[cfg(feature = "properties")]
pub mod properties;
mod properties_redaction;

mod anchor_store;
mod anchors;
mod base64;
pub mod budget;
mod de;
mod de_error;
#[path = "de/snippet.rs"]
mod de_snippet;
mod include;
mod indentation;
mod input_source;
mod live_events;
#[path = "localizer.rs"]
pub mod localizer;
mod long_strings;
mod message_formatters;
pub mod options;
mod parse_scalars;
#[cfg(feature = "include_fs")]
mod safe_resolver;
pub mod ser;
mod spanned;

#[cfg(feature = "include")]
mod include_stack;

#[cfg(any(feature = "garde", feature = "validator"))]
pub mod path_map;

pub mod ser_error;

pub use de::YamlDeserializer as Deserializer;
pub use ser::YamlSerializer as Serializer;

pub use de::{
    with_deserializer_from_reader, with_deserializer_from_reader_with_options,
    with_deserializer_from_slice, with_deserializer_from_slice_with_options,
    with_deserializer_from_str, with_deserializer_from_str_with_options,
};

mod macros;
mod serializer_options;
mod tags;

pub(crate) mod ser_quoting;

mod buffered_input;
mod location;
#[cfg(feature = "robotics")]
pub mod robotics;

#[cfg(feature = "miette")]
pub mod miette;

#[cfg(feature = "figment")]
pub mod figment;
pub(crate) mod ring_reader;
mod wrapping;
mod zmij_format;
// ---------------- Serialization (public API) ----------------

/// Serialize a value to a YAML `String`.
///
/// This is the easiest entry point when you just want a YAML string.
///
/// Example
///
/// ```rust
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Foo { a: i32, b: bool }
///
/// let s = serde_saphyr::to_string(&Foo { a: 1, b: true }).unwrap();
/// assert!(s.contains("a: 1"));
/// ```
pub fn to_string<T: serde::Serialize>(value: &T) -> std::result::Result<String, crate::ser::Error> {
    let mut out = String::new();
    to_fmt_writer(&mut out, value)?;
    Ok(out)
}

/// Serialize a value to a YAML `String`, with [`SerializerOptions`].
///
/// This is like [`to_string`], but lets you control formatting and serialization
/// behavior through the provided `options`.
///
/// Example
///
/// ```rust
/// use serde::Serialize;
/// use serde_saphyr::SerializerOptions;
///
/// #[derive(Serialize)]
/// struct Foo { a: i32, b: bool }
///
/// let options = SerializerOptions::default();
/// let s = serde_saphyr::to_string_with_options(&Foo { a: 1, b: true }, options).unwrap();
/// assert!(s.contains("a: 1"));
/// ```
pub fn to_string_with_options<T: serde::Serialize>(
    value: &T,
    options: SerializerOptions,
) -> std::result::Result<String, crate::ser::Error> {
    let mut out = String::new();
    to_fmt_writer_with_options(&mut out, value, options)?;
    Ok(out)
}

/// Deprecated: use `to_fmt_writer` or `to_io_writer`
/// Kept for a transition release to avoid instant breakage.
#[deprecated(
    since = "0.0.7",
    note = "Use `to_fmt_writer` for `fmt::Write` (String, fmt::Formatter) or `to_io_writer` for files/sockets."
)]
pub fn to_writer<W: std::fmt::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
) -> std::result::Result<(), crate::ser::Error> {
    let mut ser = crate::ser::YamlSerializer::new(output);
    value.serialize(&mut ser)
}

/// Serialize a value as YAML into any [`fmt::Write`] target.
pub fn to_fmt_writer<W: std::fmt::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
) -> std::result::Result<(), crate::ser::Error> {
    to_fmt_writer_with_options(output, value, SerializerOptions::default())
}

/// Serialize a value as YAML into any [`io::Write`] target.
pub fn to_io_writer<W: std::io::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
) -> std::result::Result<(), crate::ser::Error> {
    to_io_writer_with_options(output, value, SerializerOptions::default())
}

/// Serialize a value as YAML into any [`fmt::Write`] target, with options.
/// Options are consumed because anchor generator may be taken from them.
pub fn to_fmt_writer_with_options<W: std::fmt::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
    mut options: SerializerOptions,
) -> std::result::Result<(), crate::ser::Error> {
    options.consistent()?;
    let mut ser = crate::ser::YamlSerializer::with_options(output, &mut options);
    value.serialize(&mut ser)
}

/// Serialize a value as YAML into any [`io::Write`] target, with options.
/// Options are consumed because anchor generator may be taken from them.
pub fn to_io_writer_with_options<W: std::io::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
    mut options: SerializerOptions,
) -> std::result::Result<(), crate::ser::Error> {
    options.consistent()?;
    struct Adapter<'a, W: std::io::Write> {
        output: &'a mut W,
        last_err: Option<std::io::Error>,
    }
    impl<'a, W: std::io::Write> std::fmt::Write for Adapter<'a, W> {
        fn write_str(&mut self, s: &str) -> std::fmt::Result {
            if let Err(e) = self.output.write_all(s.as_bytes()) {
                self.last_err = Some(e);
                return Err(std::fmt::Error);
            }
            Ok(())
        }
        fn write_char(&mut self, c: char) -> std::fmt::Result {
            let mut buf = [0u8; 4];
            let s = c.encode_utf8(&mut buf);
            self.write_str(s)
        }
    }
    let mut adapter = Adapter {
        output,
        last_err: None,
    };
    let mut ser = crate::ser::YamlSerializer::with_options(&mut adapter, &mut options);
    match value.serialize(&mut ser) {
        Ok(()) => Ok(()),
        Err(e) => {
            if let Some(io_error) = adapter.last_err.take() {
                return Err(crate::ser::Error::from(io_error));
            }
            Err(e)
        }
    }
}

/// Deprecated: use `to_fmt_writer_with_options` for `fmt::Write` or `to_io_writer_with_options` for `io::Write`.
#[deprecated(
    since = "0.0.7",
    note = "Use `to_fmt_writer_with_options` for fmt::Write or `to_io_writer_with_options` for io::Write."
)]
pub fn to_writer_with_options<W: std::fmt::Write, T: serde::Serialize>(
    output: &mut W,
    value: &T,
    options: SerializerOptions,
) -> std::result::Result<(), crate::ser::Error> {
    to_fmt_writer_with_options(output, value, options)
}

/// Deserialize any `T: serde::de::Deserialize<'de>` directly from a YAML string.
///
/// This is the simplest entry point; it parses a single YAML document. If the
/// input contains multiple documents, this returns an error advising to use
/// [`from_multiple`] or [`from_multiple_with_options`].
///
/// This function supports both owned types (like `String`) and borrowed types
/// (like `&str`). For borrowed types, the deserialized value's lifetime is tied
/// to the input string's lifetime.
///
/// **Note**: Borrowing only works for simple plain scalars that don't require
/// any transformation (no multi-line folding, no escape processing). For
/// transformed strings, deserialization to `&str` will fail with a helpful
/// error message suggesting to use `String` or `Cow<str>` instead.
///
/// Example: read a small `Config` structure from a YAML string.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
///     name: My Application
///     enabled: true
///     retries: 5
/// "#;
///
/// let cfg: Config = serde_saphyr::from_str(yaml).unwrap();
/// assert!(cfg.enabled);
/// ```
///
/// Example: read a structure with borrowed string fields.
///
/// Borrowed strings are supported when deserializing from an in-memory input (`from_str` / `from_slice`),
/// and only when the scalar exists verbatim in the input (i.e., no escape processing, folding, or other
/// normalization is required). If the YAML scalar requires transformation, deserializing into `&str`
/// fails with an error suggesting `String` or `Cow<str>`.
///
/// Note: reader-based entry points like [`from_reader`] require `DeserializeOwned` and therefore cannot
/// return values that borrow from the input.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Data<'a> {
///     name: &'a str,
///     value: i32,
/// }
///
/// let yaml = "name: hello\nvalue: 42\n";
///
/// let data: Data = serde_saphyr::from_str(yaml).unwrap();
/// assert_eq!(data.name, "hello");
/// assert_eq!(data.value, 42);
/// ```
pub fn from_str<'de, T>(input: &'de str) -> Result<T, Error>
where
    T: serde::de::Deserialize<'de>,
{
    from_str_with_options(input, Options::default())
}

#[allow(deprecated)]
fn from_str_with_options_impl<'de, T>(input: &'de str, options: Options) -> Result<T, Error>
where
    T: serde::de::Deserialize<'de>,
{
    // Normalize: ignore a single leading UTF-8 BOM if present.
    let input = if let Some(rest) = input.strip_prefix('\u{FEFF}') {
        rest
    } else {
        input
    };

    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;

    let cfg = crate::de::Cfg::from_options(&options);
    // Do not stop at DocumentEnd; we'll probe for trailing content/errors explicitly.
    let mut src = LiveEvents::from_str(input, options, false);
    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            crate::de::with_root_redaction(crate::de::YamlDeserializer::new(&mut src, cfg), |de| {
                T::deserialize(de)
            })
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                let err = Error::eof().with_location(src.last_location());
                return Err(maybe_with_snippet_from_events(
                    err,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            } else {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    };

    match src.peek() {
        Ok(Some(_)) => {
            let err = Error::multiple_documents("use from_multiple or from_multiple_with_options")
                .with_location(src.last_location());
            return Err(maybe_with_snippet_from_events(
                err,
                input,
                &src,
                with_snippet,
                crop_radius,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if src.seen_doc_end() {
                // Trailing garbage after a proper document end marker is ignored.
            } else {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    }

    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }
    Ok(value)
}

/// Deserialize a single YAML document with configurable [`Options`].
///
/// This function supports both owned types (like `String`) and borrowed types
/// (like `&str`). For borrowed types, the deserialized value's lifetime is tied
/// to the input string's lifetime.
///
/// Example: read a small `Config` with a custom budget and default duplicate-key policy.
///
/// ```rust
/// use serde::Deserialize;
/// use serde_saphyr::DuplicateKeyPolicy;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
///      name: My Application
///      enabled: true
///      retries: 5
/// "#;
///
/// let options = serde_saphyr::options! {
///     budget: serde_saphyr::budget! {
///         max_anchors: 200,
///     },
///     duplicate_keys: DuplicateKeyPolicy::FirstWins,
/// };
/// let cfg: Config = serde_saphyr::from_str_with_options(yaml, options).unwrap();
/// assert_eq!(cfg.retries, 5);
/// ```
#[allow(deprecated)]
pub fn from_str_with_options<'de, T>(input: &'de str, options: Options) -> Result<T, Error>
where
    T: serde::de::Deserialize<'de>,
{
    from_str_with_options_impl(input, options)
}

/// Deserialize a single YAML document with configurable [`Options`], and also
/// return a map from validation paths to source [`Location`]s.
#[cfg(feature = "garde")]
#[allow(deprecated)]
fn from_str_with_options_and_path_recorder_garde_valid<'de, T, F>(
    input: &'de str,
    options: Options,
    validate: F,
) -> Result<(T, LiveEvents<'de>), Error>
where
    T: DeserializeOwned + garde::Validate,
    F: FnOnce(&T) -> Result<(), garde::Report>,
{
    let input = if let Some(rest) = input.strip_prefix('\u{FEFF}') {
        rest
    } else {
        input
    };

    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let cfg = crate::de::Cfg::from_options(&options);
    let mut src = LiveEvents::from_str(input, options, false);
    let mut recorder = crate::path_map::PathRecorder::new();

    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            let value = crate::de::with_root_redaction(
                crate::de::YamlDeserializer::new_with_path_recorder(&mut src, cfg, &mut recorder),
                |de| T::deserialize(de),
            )?;
            validate(&value).map_err(|report| Error::ValidationError {
                issues: collect_garde_issues(&report)
                    .into_iter()
                    .map(redact_issue)
                    .collect(),
                locations: recorder.map.clone(),
            })?;
            Ok(value)
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                let err = Error::eof().with_location(src.last_location());
                return Err(maybe_with_snippet_from_events(
                    err,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            } else {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    };

    match src.peek() {
        Ok(Some(_)) => {
            let err = Error::multiple_documents("use from_multiple or from_multiple_with_options")
                .with_location(src.last_location());
            return Err(maybe_with_snippet_from_events(
                err,
                input,
                &src,
                with_snippet,
                crop_radius,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if !src.seen_doc_end() {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    }

    Ok((value, src))
}

#[cfg(feature = "validator")]
#[allow(deprecated)]
fn from_str_with_options_and_path_recorder_validator_valid<'de, T>(
    input: &'de str,
    options: Options,
) -> Result<(T, LiveEvents<'de>), Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    let input = if let Some(rest) = input.strip_prefix('\u{FEFF}') {
        rest
    } else {
        input
    };

    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let cfg = crate::de::Cfg::from_options(&options);
    let mut src = LiveEvents::from_str(input, options, false);
    let mut recorder = crate::path_map::PathRecorder::new();

    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            let value = crate::de::with_root_redaction(
                crate::de::YamlDeserializer::new_with_path_recorder(&mut src, cfg, &mut recorder),
                |de| T::deserialize(de),
            )?;
            ValidatorValidate::validate(&value).map_err(|errors| Error::ValidatorError {
                issues: collect_validator_issues(&errors)
                    .into_iter()
                    .map(redact_issue)
                    .collect(),
                locations: recorder.map.clone(),
            })?;
            Ok(value)
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                let err = Error::eof().with_location(src.last_location());
                return Err(maybe_with_snippet_from_events(
                    err,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            } else {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    };

    match src.peek() {
        Ok(Some(_)) => {
            let err = Error::multiple_documents("use from_multiple or from_multiple_with_options")
                .with_location(src.last_location());
            return Err(maybe_with_snippet_from_events(
                err,
                input,
                &src,
                with_snippet,
                crop_radius,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if !src.seen_doc_end() {
                return Err(maybe_with_snippet_from_events(
                    e,
                    input,
                    &src,
                    with_snippet,
                    crop_radius,
                ));
            }
        }
    }

    Ok((value, src))
}

/// Deserialize a single YAML document from a YAML string and validate it with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_str_valid<T>(input: &str) -> Result<T, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    from_str_with_options_valid(input, Options::default())
}

/// Deserialize a single YAML document with configurable [`Options`] and validate it with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_str_with_options_valid<T>(input: &str, options: Options) -> Result<T, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let (v, mut src) =
        from_str_with_options_and_path_recorder_garde_valid::<T, _>(input, options, |value| {
            Validate::validate(value)
        })?;
    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }
    Ok(v)
}

/// Deserialize a single YAML document with configurable [`Options`] and validate it with `garde` in context [`<T as garde::Validate>::Context`].
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_str_with_options_context_valid<T>(
    input: &str,
    options: Options,
    context: &<T as garde::Validate>::Context,
) -> Result<T, Error>
where
    T: DeserializeOwned + garde::Validate,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let (v, mut src) =
        from_str_with_options_and_path_recorder_garde_valid::<T, _>(input, options, |value| {
            Validate::validate_with(value, context)
        })?;
    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }
    Ok(v)
}

/// Deserialize multiple YAML documents from a YAML string and validate each with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_multiple_valid<T: DeserializeOwned + garde::Validate>(
    input: &str,
) -> Result<Vec<T>, Error>
where
    <T as garde::Validate>::Context: Default,
{
    from_multiple_with_options_valid(input, Options::default())
}

/// Deserialize multiple YAML documents with configurable [`Options`] and validate each with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
#[allow(deprecated)]
pub fn from_multiple_with_options_valid<T>(input: &str, options: Options) -> Result<Vec<T>, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;

    let cfg = crate::de::Cfg::from_options(&options);
    let mut src = LiveEvents::from_str(input, options, false);
    let mut values = Vec::new();
    let mut validation_errors: Vec<Error> = Vec::new();

    loop {
        match src.peek()? {
            // Skip documents that are explicit null-like scalars ("", "~", or "null").
            Some(Ev::Scalar {
                value: s,
                style,
                tag,
                ..
            }) if *tag == crate::tags::SfTag::Null
                || (*tag != crate::tags::SfTag::String && scalar_is_nullish(s, style)) =>
            {
                let _ = src.next()?; // consume the null scalar document
                continue;
            }
            Some(_) => {
                let mut recorder = crate::path_map::PathRecorder::new();
                let value_res = crate::anchor_store::with_document_scope(|| {
                    with_interp_redaction_scope(|| {
                        let value = crate::de::with_root_redaction(
                            crate::de::YamlDeserializer::new_with_path_recorder(
                                &mut src,
                                cfg,
                                &mut recorder,
                            ),
                            |de| T::deserialize(de),
                        )?;
                        Validate::validate(&value).map_err(|report| Error::ValidationError {
                            issues: collect_garde_issues(&report)
                                .into_iter()
                                .map(redact_issue)
                                .collect(),
                            locations: recorder.map.clone(),
                        })?;
                        Ok(value)
                    })
                });
                let value = match value_res {
                    Ok(v) => v,
                    Err(Error::ValidationError { issues, locations }) => {
                        let err = Error::ValidationError { issues, locations };
                        validation_errors.push(maybe_with_snippet_from_events(
                            err,
                            input,
                            &src,
                            with_snippet,
                            crop_radius,
                        ));
                        continue;
                    }
                    Err(e) => {
                        return Err(maybe_with_snippet_from_events(
                            e,
                            input,
                            &src,
                            with_snippet,
                            crop_radius,
                        ));
                    }
                };

                values.push(value);
            }
            None => break,
        }
    }

    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }

    if validation_errors.is_empty() {
        Ok(values)
    } else {
        Err(Error::ValidationErrors {
            errors: validation_errors,
        })
    }
}

/// Deserialize a single YAML document from bytes and validate it with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_slice_valid<T: DeserializeOwned + garde::Validate>(bytes: &[u8]) -> Result<T, Error>
where
    <T as garde::Validate>::Context: Default,
{
    from_slice_with_options_valid(bytes, Options::default())
}

/// Deserialize a single YAML document from bytes and validate it with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_slice_with_options_valid<T: DeserializeOwned + garde::Validate>(
    bytes: &[u8],
    options: Options,
) -> Result<T, Error>
where
    <T as garde::Validate>::Context: Default,
{
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_str_with_options_valid(s, options)
}

/// Deserialize multiple YAML documents from bytes with options and validate each with `garde`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "garde")]
pub fn from_slice_multiple_with_options_valid<T>(
    bytes: &[u8],
    options: Options,
) -> Result<Vec<T>, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_multiple_with_options_valid(s, options)
}

/// Deserialize a single YAML document from a reader and validate it with `garde`.
/// Snippets are attached on a best-effort basis for streamed root input, are available for
/// included text sources, and may be unavailable for included reader sources.
#[cfg(feature = "garde")]
pub fn from_reader_valid<R: std::io::Read, T>(reader: R) -> Result<T, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    from_reader_with_options_valid(reader, Options::default())
}

/// Deserialize a single YAML document from a reader with options and validate it with `garde`.
/// Snippets are attached on a best-effort basis for streamed root input, are available for
/// included text sources, and may be unavailable for included reader sources.
#[cfg(feature = "garde")]
#[allow(deprecated)]
pub fn from_reader_with_options_valid<R: std::io::Read, T>(
    reader: R,
    options: Options,
) -> Result<T, Error>
where
    T: DeserializeOwned + garde::Validate,
    <T as garde::Validate>::Context: Default,
{
    let cfg = crate::de::Cfg::from_options(&options);
    let (snippet_ctx, ring_handle) =
        ReaderSnippetContext::new(reader, options.with_snippet, options.crop_radius);
    let mut src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::AllContent);

    let mut recorder = crate::path_map::PathRecorder::new();

    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            let value = crate::de::with_root_redaction(
                crate::de::YamlDeserializer::new_with_path_recorder(&mut src, cfg, &mut recorder),
                |de| T::deserialize(de),
            )?;
            Validate::validate(&value).map_err(|report| Error::ValidationError {
                issues: collect_garde_issues(&report)
                    .into_iter()
                    .map(redact_issue)
                    .collect(),
                locations: recorder.map.clone(),
            })?;
            Ok(value)
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                // If the only thing in the input was an empty document (synthetic null),
                // surface this as an EOF error to preserve expected error semantics
                // for incompatible target types (e.g., bool).
                return Err(snippet_ctx
                    .attach_snippet(Error::eof().with_location(src.last_location()), &src));
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    };

    // After finishing first document, peek ahead to detect either another document/content
    // or trailing garbage. If a scan error occurs but we have seen a DocumentEnd ("..."),
    // ignore the trailing garbage. Otherwise, surface the error.
    match src.peek() {
        Ok(Some(_)) => {
            return Err(snippet_ctx.attach_snippet(
                Error::multiple_documents(
                    "use read_valid or read_with_options_valid to obtain the iterator",
                )
                .with_location(src.last_location()),
                &src,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if src.seen_doc_end() {
                // Trailing garbage after a proper document end marker is ignored.
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    }

    if let Err(e) = src.finish() {
        return Err(snippet_ctx.attach_snippet(e, &src));
    }
    Ok(value)
}

/// Create an iterator over validated YAML documents from a reader.
/// Root streamed input gets snippets on a best-effort basis; included text sources retain full
/// snippets, while included reader sources may not have snippet text available.
#[cfg(feature = "garde")]
pub fn read_valid<'a, R, T>(reader: &'a mut R) -> impl Iterator<Item = Result<T, Error>> + 'a
where
    R: Read + 'a,
    T: DeserializeOwned + garde::Validate + 'a,
    <T as garde::Validate>::Context: Default,
{
    read_with_options_valid(reader, Default::default())
}

/// Create an iterator over validated YAML documents from a reader with configurable options.
/// Root streamed input gets snippets on a best-effort basis; included text sources retain full
/// snippets, while included reader sources may not have snippet text available.
#[cfg(feature = "garde")]
#[allow(deprecated)]
pub fn read_with_options_valid<'a, R, T>(
    reader: &'a mut R,
    options: Options,
) -> impl Iterator<Item = Result<T, Error>> + 'a
where
    R: Read + 'a,
    T: DeserializeOwned + garde::Validate + 'a,
    <T as garde::Validate>::Context: Default,
{
    struct ReadValidIter<'a, R, T> {
        snippet_ctx: ReaderSnippetContext<&'a mut R>,
        src: LiveEvents<'a>, // borrows from `reader`
        cfg: crate::de::Cfg,
        finished: bool,
        _marker: std::marker::PhantomData<T>,
    }

    impl<'a, R, T> Iterator for ReadValidIter<'a, R, T>
    where
        R: Read + 'a,
        T: DeserializeOwned + garde::Validate + 'a,
        <T as garde::Validate>::Context: Default,
    {
        type Item = Result<T, Error>;

        fn next(&mut self) -> Option<Self::Item> {
            if self.finished {
                return None;
            }
            loop {
                match self.src.peek() {
                    Ok(Some(Ev::Scalar { value, style, .. }))
                        if scalar_is_nullish(value, style) =>
                    {
                        let _ = self.src.next();
                        continue;
                    }
                    Ok(Some(_)) => {
                        let mut recorder = crate::path_map::PathRecorder::new();
                        let value_res = crate::anchor_store::with_document_scope(|| {
                            with_interp_redaction_scope(|| {
                                let value = T::deserialize(
                                    crate::de::YamlDeserializer::new_with_path_recorder(
                                        &mut self.src,
                                        self.cfg,
                                        &mut recorder,
                                    ),
                                )?;
                                Validate::validate(&value).map_err(|report| {
                                    Error::ValidationError {
                                        issues: collect_garde_issues(&report)
                                            .into_iter()
                                            .map(redact_issue)
                                            .collect(),
                                        locations: recorder.map.clone(),
                                    }
                                })?;
                                Ok(value)
                            })
                        });
                        let value = match value_res {
                            Ok(v) => v,
                            Err(e) => {
                                let err = self.snippet_ctx.attach_snippet(e, &self.src);
                                // After a deserialization error, skip remaining events in the
                                // current document and try to recover at the next document boundary.
                                if !self.src.skip_to_next_document() {
                                    self.finished = true;
                                }
                                return Some(Err(err));
                            }
                        };

                        return Some(Ok(value));
                    }
                    Ok(None) => {
                        self.finished = true;
                        if let Err(e) = self.src.finish() {
                            return Some(Err(self.snippet_ctx.attach_snippet(e, &self.src)));
                        }
                        return None;
                    }
                    Err(e) => {
                        self.finished = true;
                        let _ = self.src.finish();
                        return Some(Err(self.snippet_ctx.attach_snippet(e, &self.src)));
                    }
                }
            }
        }
    }

    let cfg = crate::de::Cfg::from_options(&options);
    let (snippet_ctx, ring_handle) =
        ReaderSnippetContext::new(reader, options.with_snippet, options.crop_radius);
    let src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::PerDocument);

    ReadValidIter::<R, T> {
        snippet_ctx,
        src,
        cfg,
        finished: false,
        _marker: std::marker::PhantomData,
    }
}

/// Deserialize a single YAML document from a YAML string and validate it with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_str_validate<T>(input: &str) -> Result<T, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    from_str_with_options_validate(input, Options::default())
}

/// Deserialize a single YAML document with configurable [`Options`] and validate it with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_str_with_options_validate<T>(input: &str, options: Options) -> Result<T, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let (v, mut src) =
        from_str_with_options_and_path_recorder_validator_valid::<T>(input, options)?;
    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }
    Ok(v)
}

/// Deserialize multiple YAML documents from a YAML string and validate each with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_multiple_validate<T: DeserializeOwned + ValidatorValidate>(
    input: &str,
) -> Result<Vec<T>, Error> {
    from_multiple_with_options_validate(input, Options::default())
}

/// Deserialize multiple YAML documents with configurable [`Options`] and validate each with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
#[allow(deprecated)]
pub fn from_multiple_with_options_validate<T>(
    input: &str,
    options: Options,
) -> Result<Vec<T>, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;

    let cfg = crate::de::Cfg::from_options(&options);
    let mut src = LiveEvents::from_str(input, options, false);
    let mut values = Vec::new();
    let mut validation_errors: Vec<Error> = Vec::new();

    loop {
        match src.peek()? {
            // Skip documents that are explicit null-like scalars ("", "~", or "null").
            Some(Ev::Scalar {
                value: s,
                style,
                tag,
                ..
            }) if *tag == crate::tags::SfTag::Null
                || (*tag != crate::tags::SfTag::String && scalar_is_nullish(s, style)) =>
            {
                let _ = src.next()?; // consume the null scalar document
                continue;
            }
            Some(_) => {
                let mut recorder = crate::path_map::PathRecorder::new();
                let value_res = crate::anchor_store::with_document_scope(|| {
                    with_interp_redaction_scope(|| {
                        let value = crate::de::with_root_redaction(
                            crate::de::YamlDeserializer::new_with_path_recorder(
                                &mut src,
                                cfg,
                                &mut recorder,
                            ),
                            |de| T::deserialize(de),
                        )?;
                        ValidatorValidate::validate(&value).map_err(|errors| {
                            Error::ValidatorError {
                                issues: collect_validator_issues(&errors)
                                    .into_iter()
                                    .map(redact_issue)
                                    .collect(),
                                locations: recorder.map.clone(),
                            }
                        })?;
                        Ok(value)
                    })
                });
                let value = match value_res {
                    Ok(v) => v,
                    Err(Error::ValidatorError { issues, locations }) => {
                        let err = Error::ValidatorError { issues, locations };
                        validation_errors.push(maybe_with_snippet_from_events(
                            err,
                            input,
                            &src,
                            with_snippet,
                            crop_radius,
                        ));
                        continue;
                    }
                    Err(e) => {
                        return Err(maybe_with_snippet_from_events(
                            e,
                            input,
                            &src,
                            with_snippet,
                            crop_radius,
                        ));
                    }
                };

                values.push(value);
            }
            None => break,
        }
    }

    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }

    if validation_errors.is_empty() {
        Ok(values)
    } else {
        Err(Error::ValidatorErrors {
            errors: validation_errors,
        })
    }
}

/// Deserialize a single YAML document from bytes and validate it with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_slice_validate<T: DeserializeOwned + ValidatorValidate>(
    bytes: &[u8],
) -> Result<T, Error> {
    from_slice_with_options_validate(bytes, Options::default())
}

/// Deserialize a single YAML document from bytes and validate it with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_slice_with_options_validate<T: DeserializeOwned + ValidatorValidate>(
    bytes: &[u8],
    options: Options,
) -> Result<T, Error> {
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_str_with_options_validate(s, options)
}

/// Deserialize multiple YAML documents from bytes with options and validate each with `validator`.
/// The error message will contain a snippet with exact location information, and if the
/// invalid value comes from anchor, serde-saphyr will also tell where it is defined.
#[cfg(feature = "validator")]
pub fn from_slice_multiple_with_options_validate<T>(
    bytes: &[u8],
    options: Options,
) -> Result<Vec<T>, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_multiple_with_options_validate(s, options)
}

/// Deserialize a single YAML document from a reader and validate it with `validator`.
/// Snippets are attached on a best-effort basis for streamed root input, are available for
/// included text sources, and may be unavailable for included reader sources.
#[cfg(feature = "validator")]
pub fn from_reader_validate<R: std::io::Read, T>(reader: R) -> Result<T, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    from_reader_with_options_validate(reader, Options::default())
}

/// Deserialize a single YAML document from a reader with options and validate it with `validator`.
/// Snippets are attached on a best-effort basis for streamed root input, are available for
/// included text sources, and may be unavailable for included reader sources.
#[cfg(feature = "validator")]
#[allow(deprecated)]
pub fn from_reader_with_options_validate<R: std::io::Read, T>(
    reader: R,
    options: Options,
) -> Result<T, Error>
where
    T: DeserializeOwned + ValidatorValidate,
{
    let cfg = crate::de::Cfg::from_options(&options);
    let (snippet_ctx, ring_handle) =
        ReaderSnippetContext::new(reader, options.with_snippet, options.crop_radius);
    let mut src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::AllContent);

    let mut recorder = crate::path_map::PathRecorder::new();

    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            let value = crate::de::with_root_redaction(
                crate::de::YamlDeserializer::new_with_path_recorder(&mut src, cfg, &mut recorder),
                |de| T::deserialize(de),
            )?;
            ValidatorValidate::validate(&value).map_err(|errors| Error::ValidatorError {
                issues: collect_validator_issues(&errors)
                    .into_iter()
                    .map(redact_issue)
                    .collect(),
                locations: recorder.map.clone(),
            })?;
            Ok(value)
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                // If the only thing in the input was an empty document (synthetic null),
                // surface this as an EOF error to preserve expected error semantics
                // for incompatible target types (e.g., bool).
                return Err(snippet_ctx
                    .attach_snippet(Error::eof().with_location(src.last_location()), &src));
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    };

    // After finishing first document, peek ahead to detect either another document/content
    // or trailing garbage. If a scan error occurs but we have seen a DocumentEnd ("..."),
    // ignore the trailing garbage. Otherwise, surface the error.
    match src.peek() {
        Ok(Some(_)) => {
            return Err(snippet_ctx.attach_snippet(
                Error::multiple_documents(
                    "use read_validate or read_with_options_validate to obtain the iterator",
                )
                .with_location(src.last_location()),
                &src,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if src.seen_doc_end() {
                // Trailing garbage after a proper document end marker is ignored.
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    }

    if let Err(e) = src.finish() {
        return Err(snippet_ctx.attach_snippet(e, &src));
    }
    Ok(value)
}

/// Create an iterator over validated YAML documents from a reader.
/// Root streamed input gets snippets on a best-effort basis; included text sources retain full
/// snippets, while included reader sources may not have snippet text available.
#[cfg(feature = "validator")]
pub fn read_validate<'a, R, T>(reader: &'a mut R) -> impl Iterator<Item = Result<T, Error>> + 'a
where
    R: Read + 'a,
    T: DeserializeOwned + ValidatorValidate + 'a,
{
    read_with_options_validate(reader, Default::default())
}

/// Create an iterator over validated YAML documents from a reader with configurable options.
/// Root streamed input gets snippets on a best-effort basis; included text sources retain full
/// snippets, while included reader sources may not have snippet text available.
#[cfg(feature = "validator")]
#[allow(deprecated)]
pub fn read_with_options_validate<'a, R, T>(
    reader: &'a mut R,
    options: Options,
) -> impl Iterator<Item = Result<T, Error>> + 'a
where
    R: Read + 'a,
    T: DeserializeOwned + ValidatorValidate + 'a,
{
    struct ReadValidateIter<'a, R, T> {
        snippet_ctx: ReaderSnippetContext<&'a mut R>,
        src: LiveEvents<'a>, // borrows from `reader`
        cfg: crate::de::Cfg,
        finished: bool,
        _marker: std::marker::PhantomData<T>,
    }

    impl<'a, R, T> Iterator for ReadValidateIter<'a, R, T>
    where
        R: Read + 'a,
        T: DeserializeOwned + ValidatorValidate + 'a,
    {
        type Item = Result<T, Error>;

        fn next(&mut self) -> Option<Self::Item> {
            if self.finished {
                return None;
            }
            loop {
                match self.src.peek() {
                    Ok(Some(Ev::Scalar { value, style, .. }))
                        if scalar_is_nullish(value, style) =>
                    {
                        let _ = self.src.next();
                        continue;
                    }
                    Ok(Some(_)) => {
                        let mut recorder = crate::path_map::PathRecorder::new();
                        let value_res = crate::anchor_store::with_document_scope(|| {
                            with_interp_redaction_scope(|| {
                                let value = T::deserialize(
                                    crate::de::YamlDeserializer::new_with_path_recorder(
                                        &mut self.src,
                                        self.cfg,
                                        &mut recorder,
                                    ),
                                )?;
                                ValidatorValidate::validate(&value).map_err(|errors| {
                                    Error::ValidatorError {
                                        issues: collect_validator_issues(&errors)
                                            .into_iter()
                                            .map(redact_issue)
                                            .collect(),
                                        locations: recorder.map.clone(),
                                    }
                                })?;
                                Ok(value)
                            })
                        });
                        let value = match value_res {
                            Ok(v) => v,
                            Err(e) => {
                                let err = self.snippet_ctx.attach_snippet(e, &self.src);
                                // After a deserialization error, skip remaining events in the
                                // current document and try to recover at the next document boundary.
                                if !self.src.skip_to_next_document() {
                                    self.finished = true;
                                }
                                return Some(Err(err));
                            }
                        };

                        return Some(Ok(value));
                    }
                    Ok(None) => {
                        self.finished = true;
                        if let Err(e) = self.src.finish() {
                            return Some(Err(self.snippet_ctx.attach_snippet(e, &self.src)));
                        }
                        return None;
                    }
                    Err(e) => {
                        self.finished = true;
                        let _ = self.src.finish();
                        return Some(Err(self.snippet_ctx.attach_snippet(e, &self.src)));
                    }
                }
            }
        }
    }

    let cfg = crate::de::Cfg::from_options(&options);
    let (snippet_ctx, ring_handle) =
        ReaderSnippetContext::new(reader, options.with_snippet, options.crop_radius);
    let src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::PerDocument);

    ReadValidateIter::<R, T> {
        snippet_ctx,
        src,
        cfg,
        finished: false,
        _marker: std::marker::PhantomData,
    }
}

pub(crate) fn maybe_with_snippet(
    err: Error,
    input: &str,
    with_snippet: bool,
    crop_radius: usize,
) -> Error {
    if !(with_snippet && crop_radius > 0 && err.location().is_some()) {
        return err;
    }

    err.with_snippet(input, crop_radius)
}

pub(crate) struct RootFragment<'a> {
    pub text: &'a str,
    pub start_line: usize,
    pub source_name: &'a str,
}

struct ReaderSnippetContext<R> {
    shared_ring: ring_reader::SharedRingReader<R>,
    with_snippet: bool,
    crop_radius: usize,
}

impl<R: Read> ReaderSnippetContext<R> {
    fn new(
        reader: R,
        with_snippet: bool,
        crop_radius: usize,
    ) -> (Self, ring_reader::SharedRingReaderHandle<R>) {
        let shared_ring = ring_reader::SharedRingReader::new(reader);
        let ring_handle = ring_reader::SharedRingReaderHandle::new(&shared_ring);
        (
            Self {
                shared_ring,
                with_snippet,
                crop_radius,
            },
            ring_handle,
        )
    }

    fn attach_snippet(&self, err: Error, src: &LiveEvents<'_>) -> Error {
        if !self.with_snippet || self.crop_radius == 0 {
            return err;
        }

        match self.shared_ring.get_recent() {
            Ok(snapshot) => {
                let text = String::from_utf8_lossy(&snapshot.bytes);
                let root = RootFragment {
                    text: text.as_ref(),
                    start_line: snapshot.start_line,
                    source_name: "input",
                };
                maybe_with_snippet_from_events_and_root_fragment(
                    err,
                    Some(&root),
                    text.as_ref(),
                    src,
                    self.with_snippet,
                    self.crop_radius,
                )
            }
            Err(_) => err,
        }
    }
}

#[cfg(feature = "include")]
fn with_root_additional_snippet(
    err: Error,
    root: Option<&RootFragment<'_>>,
    input: &str,
    location: &crate::Location,
    crop_radius: usize,
) -> Error {
    match root {
        Some(root) => err.with_additional_snippet_offset_named(
            root.text,
            root.start_line,
            root.source_name,
            location,
            crop_radius,
        ),
        None => err.with_additional_snippet_named(input, "input", location, crop_radius),
    }
}

#[cfg(feature = "include")]
fn recorded_source_snippet_chain<'a>(
    events: &'a crate::live_events::LiveEvents<'_>,
    location: &crate::Location,
) -> Option<Vec<&'a crate::include_stack::RecordedSource>> {
    let chain = events.recorded_source_chain(location.source_id());
    chain.first()?.text.as_deref()?;
    Some(chain)
}

#[cfg(feature = "include")]
fn with_recorded_source_snippets(
    err: Error,
    root: Option<&RootFragment<'_>>,
    input: &str,
    chain: &[&crate::include_stack::RecordedSource],
    crop_radius: usize,
) -> Error {
    let current = chain[0];
    let source_text = current
        .text
        .as_deref()
        .expect("recorded source snippet chain must start with text-backed source");
    let mut err_with_snippet =
        err.with_snippet_named(source_text, current.name.as_str(), crop_radius);

    for window in chain.windows(2) {
        let child = window[0];
        let parent = window[1];
        if child.include_location == crate::Location::UNKNOWN {
            continue;
        }

        match parent.text.as_deref() {
            Some(parent_text) => {
                err_with_snippet = err_with_snippet.with_additional_snippet_named(
                    parent_text,
                    parent.name.as_str(),
                    &child.include_location,
                    crop_radius,
                );
            }
            None if parent.parent_source_id.is_none() => {
                err_with_snippet = with_root_additional_snippet(
                    err_with_snippet,
                    root,
                    input,
                    &child.include_location,
                    crop_radius,
                );
            }
            None => {}
        }
    }
    err_with_snippet
}

pub(crate) fn maybe_with_snippet_from_events_and_root_fragment(
    err: Error,
    root: Option<&RootFragment<'_>>,
    input: &str,
    #[allow(unused_variables)] events: &crate::live_events::LiveEvents<'_>,
    with_snippet: bool,
    crop_radius: usize,
) -> Error {
    if !(with_snippet && crop_radius > 0 && err.location().is_some()) {
        return err;
    }

    #[cfg(feature = "include")]
    if let Some(loc) = err.location()
        && let Some(chain) = recorded_source_snippet_chain(events, &loc)
    {
        return with_recorded_source_snippets(err, root, input, &chain, crop_radius);
    }

    match root {
        Some(root) => {
            err.with_snippet_offset_named(root.text, root.start_line, root.source_name, crop_radius)
        }
        None => maybe_with_snippet(err, input, with_snippet, crop_radius),
    }
}

pub(crate) fn maybe_with_snippet_from_events(
    err: Error,
    input: &str,
    #[allow(unused_variables)] events: &crate::live_events::LiveEvents<'_>,
    with_snippet: bool,
    crop_radius: usize,
) -> Error {
    maybe_with_snippet_from_events_and_root_fragment(
        err,
        None,
        input,
        events,
        with_snippet,
        crop_radius,
    )
}

/// Deserialize multiple YAML documents from a single string into a vector of `T`.
/// Completely empty documents are ignored and not included into returned vector.
///
/// Example: read two `Config` documents separated by `---`.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
/// name: First
/// enabled: true
/// retries: 1
/// ---
/// name: Second
/// enabled: false
/// retries: 2
/// "#;
///
/// let cfgs: Vec<Config> = serde_saphyr::from_multiple(yaml).unwrap();
/// assert_eq!(cfgs.len(), 2);
/// assert_eq!(cfgs[0].name, "First");
/// ```
pub fn from_multiple<T: DeserializeOwned>(input: &str) -> Result<Vec<T>, Error> {
    from_multiple_with_options(input, Options::default())
}

/// Deserialize multiple YAML documents into a vector with configurable [`Options`].
///
/// Example: two `Config` documents with a custom budget.
///
/// ```rust
/// use serde::Deserialize;
/// use serde_saphyr::DuplicateKeyPolicy;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
/// name: First
/// enabled: true
/// retries: 1
/// ---
/// name: Second
/// enabled: false
/// retries: 2
/// "#;
///
/// let options = serde_saphyr::options! {
///     budget: serde_saphyr::budget! {
///         max_anchors: 200,
///     },
///     duplicate_keys: DuplicateKeyPolicy::FirstWins,
/// };
/// let cfgs: Vec<Config> = serde_saphyr::from_multiple_with_options(yaml, options).unwrap();
/// assert_eq!(cfgs.len(), 2);
/// assert!(!cfgs[1].enabled);
/// ```
#[allow(deprecated)]
pub fn from_multiple_with_options<T: DeserializeOwned>(
    input: &str,
    options: Options,
) -> Result<Vec<T>, Error> {
    // Normalize: ignore a single leading UTF-8 BOM if present.
    let input = if let Some(rest) = input.strip_prefix('\u{FEFF}') {
        rest
    } else {
        input
    };
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;

    let cfg = crate::de::Cfg::from_options(&options);
    let mut src = LiveEvents::from_str(input, options, false);
    let mut values = Vec::new();

    loop {
        match src.peek()? {
            // Skip documents that are explicit null-like scalars ("", "~", or "null").
            Some(Ev::Scalar {
                value: s,
                style,
                tag,
                ..
            }) if *tag == crate::tags::SfTag::Null
                || (*tag != crate::tags::SfTag::String && scalar_is_nullish(s, style)) =>
            {
                let _ = src.next()?; // consume the null scalar document
                // Do not push anything for this document; move to the next one.
                continue;
            }
            Some(_) => {
                let value_res = crate::anchor_store::with_document_scope(|| {
                    with_interp_redaction_scope(|| {
                        crate::de::with_root_redaction(
                            crate::de::YamlDeserializer::new(&mut src, cfg),
                            |de| T::deserialize(de),
                        )
                    })
                });
                let value = match value_res {
                    Ok(v) => v,
                    Err(e) => {
                        return Err(maybe_with_snippet_from_events(
                            e,
                            input,
                            &src,
                            with_snippet,
                            crop_radius,
                        ));
                    }
                };
                values.push(value);
            }
            None => break,
        }
    }

    if let Err(e) = src.finish() {
        return Err(maybe_with_snippet_from_events(
            e,
            input,
            &src,
            with_snippet,
            crop_radius,
        ));
    }
    Ok(values)
}

/// Deserialize a single YAML document from a UTF-8 byte slice.
///
/// This is equivalent to [`from_str`], but accepts `&[u8]` and validates it is
/// valid UTF-8 before parsing.
///
/// Example: read a small `Config` structure from bytes.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
/// name: My Application
/// enabled: true
/// retries: 5
/// "#;
/// let bytes = yaml.as_bytes();
/// let cfg: Config = serde_saphyr::from_slice(bytes).unwrap();
/// assert!(cfg.enabled);
/// ```
///
pub fn from_slice<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, Error> {
    from_slice_with_options(bytes, Options::default())
}

/// Deserialize a single YAML document from a UTF-8 byte slice with configurable [`Options`].
///
/// Example: read a small `Config` with a custom budget from bytes.
///
/// ```rust
/// use serde::Deserialize;
/// use serde_saphyr::DuplicateKeyPolicy;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
///      name: My Application
///      enabled: true
///      retries: 5
/// "#;
/// let bytes = yaml.as_bytes();
/// let options = serde_saphyr::options! {
///     budget: serde_saphyr::budget! {
///         max_anchors: 200,
///     },
///     duplicate_keys: DuplicateKeyPolicy::FirstWins,
/// };
/// let cfg: Config = serde_saphyr::from_slice_with_options(bytes, options).unwrap();
/// assert_eq!(cfg.retries, 5);
/// ```
pub fn from_slice_with_options<'de, T>(bytes: &'de [u8], options: Options) -> Result<T, Error>
where
    T: serde::Deserialize<'de>,
{
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_str_with_options(s, options)
}

/// Deserialize multiple YAML documents from a UTF-8 byte slice into a vector of `T`.
///
/// Example: read two `Config` documents separated by `---` from bytes.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
/// name: First
/// enabled: true
/// retries: 1
/// ---
/// name: Second
/// enabled: false
/// retries: 2
/// "#;
/// let bytes = yaml.as_bytes();
/// let cfgs: Vec<Config> = serde_saphyr::from_slice_multiple(bytes).unwrap();
/// assert_eq!(cfgs.len(), 2);
/// assert_eq!(cfgs[0].name, "First");
/// ```
pub fn from_slice_multiple<T: DeserializeOwned>(bytes: &[u8]) -> Result<Vec<T>, Error> {
    from_slice_multiple_with_options(bytes, Options::default())
}

/// Deserialize multiple YAML documents from bytes with configurable [`Options`].
/// Completely empty documents are ignored and not included into returned vector.
///
/// Example: two `Config` documents with a custom budget from bytes.
///
/// ```rust
/// use serde::Deserialize;
/// use serde_saphyr::DuplicateKeyPolicy;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Config {
///     name: String,
///     enabled: bool,
///     retries: i32,
/// }
///
/// let yaml = r#"
/// name: First
/// enabled: true
/// retries: 1
/// ---
/// name: Second
/// enabled: false
/// retries: 2
/// "#;
/// let bytes = yaml.as_bytes();
/// let options = serde_saphyr::options! {
///     budget: serde_saphyr::budget! {
///         max_anchors: 200,
///     },
///     duplicate_keys: DuplicateKeyPolicy::FirstWins,
/// };
/// let cfgs: Vec<Config> = serde_saphyr::from_slice_multiple_with_options(bytes, options).unwrap();
/// assert_eq!(cfgs.len(), 2);
/// assert!(!cfgs[1].enabled);
/// ```
pub fn from_slice_multiple_with_options<T: DeserializeOwned>(
    bytes: &[u8],
    options: Options,
) -> Result<Vec<T>, Error> {
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    from_multiple_with_options(s, options)
}

/// Serialize multiple documents into a YAML string.
///
/// Serializes each value in the provided slice as an individual YAML document.
/// Documents are separated by a standard YAML document start marker ("---\n").
/// No marker is emitted before the first document.
///
/// Example
///
/// ```rust
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Point { x: i32 }
///
/// let docs = vec![Point { x: 1 }, Point { x: 2 }];
/// let out = serde_saphyr::to_string_multiple(&docs).unwrap();
/// assert_eq!(out, "x: 1\n---\nx: 2\n");
/// ```
pub fn to_string_multiple<T: serde::Serialize>(
    values: &[T],
) -> std::result::Result<String, crate::ser::Error> {
    to_string_multiple_with_options(values, SerializerOptions::default())
}

/// Serialize multiple documents into a YAML string with configurable `Options`.
///
/// Serializes each value in the provided slice as an individual YAML document.
/// Documents are separated by a standard YAML document start marker ("---\n").
/// No marker is emitted before the first document.
///
/// Example
///
/// ```rust
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Point { coords: Vec<i32> }
///
/// let docs = vec![Point { coords: vec![0,1] }, Point { coords: vec![3,2] }];
/// let options = serde_saphyr::ser_options! {
///     indent_step: 2,
///     compact_list_indent: true
/// };
/// let out = serde_saphyr::to_string_multiple_with_options(&docs, options).unwrap();
/// assert_eq!(out, "coords:\n- 0\n- 1\n---\ncoords:\n- 3\n- 2\n");
/// ```
pub fn to_string_multiple_with_options<T: serde::Serialize>(
    values: &[T],
    options: SerializerOptions,
) -> std::result::Result<String, crate::ser::Error> {
    let mut out = String::new();
    let mut first = true;
    for v in values {
        if !first {
            out.push_str("---\n");
        }
        first = false;
        to_fmt_writer_with_options(&mut out, v, options)?;
    }
    Ok(out)
}

/// Deserialize a single YAML document from any `std::io::Read`.
///
///
/// This method parsers as it reads, without loading the entire input into memory first. Hence,
/// budget limits protect against large (potentially malicious) input.
///
/// Example
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use std::collections::HashMap;
/// use serde_json::Value;
///
/// #[derive(Debug, PartialEq, Serialize, Deserialize)]
/// struct Point {
///     x: i32,
///     y: i32,
/// }
///
/// let yaml = "x: 3\ny: 4\n";
/// let reader = std::io::Cursor::new(yaml.as_bytes());
/// let p: Point = serde_saphyr::from_reader(reader).unwrap();
/// assert_eq!(p, Point { x: 3, y: 4 });
///
/// // It also works for dynamic values like serde_json::Value
/// let mut big = String::new();
/// let mut i = 0usize;
/// while big.len() < 64 * 1024 { big.push_str(&format!("k{0}: v{0}\n", i)); i += 1; }
/// let reader = std::io::Cursor::new(big.as_bytes().to_owned());
/// let _value: Value = serde_saphyr::from_reader(reader).unwrap();
/// ```
pub fn from_reader<'a, R: std::io::Read + 'a, T: DeserializeOwned>(reader: R) -> Result<T, Error> {
    from_reader_with_options(reader, Options::default())
}

/// Deserialize a single YAML document from any `std::io::Read` with configurable `Options`.
///
/// This is the reader-based counterpart to [`from_str_with_options`]. It consumes a
/// byte-oriented reader, decodes it to UTF-8, and streams events into the deserializer.
///
/// This method parsers as it reads, without loading the entire input into memory first. Hence,
/// budget limits protect against large (potentially malicious) input.
///
/// Notes on limits and large inputs
/// - Parsing limits: Use [`Options::budget`] to constrain YAML complexity (events, nodes,
///   nesting depth, total scalar bytes, number of documents, anchors, aliases, etc.). These
///   limits are enforced during parsing and are enabled by default via `Options::default()`.
/// - Byte-level input cap: from_slice_multiple hard cap on input bytes is enforced via `Options::budget.max_reader_input_bytes`.
///   The default budget sets this to 256 MiB. You can override it by customizing `Options::budget`.
///   When the cap is exceeded, deserialization fails early with a budget error.
///
/// Example: limit raw input bytes and customize options
/// ```rust
/// use std::io::{Read, Cursor};
/// use serde::Deserialize;
/// use serde_saphyr::{Budget, Options};
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Point { x: i32, y: i32 }
///
/// let yaml = "x: 3\ny: 4\n";
/// let reader = Cursor::new(yaml.as_bytes());
///
/// let opts = serde_saphyr::options! {
///     budget: serde_saphyr::budget! {
///         max_events: 10_000,
///         max_reader_input_bytes: Some(1024),
///     },
/// };
///
/// let p: Point = serde_saphyr::from_reader_with_options(reader, opts).unwrap();
/// assert_eq!(p, Point { x: 3, y: 4 });
/// ```
///
/// Error behavior
/// - If an empty document is provided (no content), a type-mismatch (eof) error is returned when
///   attempting to deserialize into non-null-like targets.
/// - If the reader contains multiple documents, an error is returned suggesting the
///   `read`/`read_with_options` iterator APIs.
/// - If `Options::budget` is set and a limit is exceeded, an error is returned early.
#[allow(deprecated)]
pub fn from_reader_with_options<'a, R: std::io::Read + 'a, T: DeserializeOwned>(
    reader: R,
    options: Options,
) -> Result<T, Error> {
    let cfg = crate::de::Cfg::from_options(&options);
    let (snippet_ctx, ring_handle) =
        ReaderSnippetContext::new(reader, options.with_snippet, options.crop_radius);

    let mut src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::AllContent);

    let value_res = crate::anchor_store::with_document_scope(|| {
        with_interp_redaction_scope(|| {
            crate::de::with_root_redaction(crate::de::YamlDeserializer::new(&mut src, cfg), |de| {
                T::deserialize(de)
            })
        })
    });
    let value = match value_res {
        Ok(v) => v,
        Err(e) => {
            if src.synthesized_null_emitted() {
                // If the only thing in the input was an empty document (synthetic null),
                // surface this as an EOF error to preserve expected error semantics
                // for incompatible target types (e.g., bool).
                return Err(snippet_ctx
                    .attach_snippet(Error::eof().with_location(src.last_location()), &src));
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    };

    // After finishing first document, peek ahead to detect either another document/content
    // or trailing garbage. If a scan error occurs but we have seen a DocumentEnd ("..."),
    // ignore the trailing garbage. Otherwise, surface the error.
    match src.peek() {
        Ok(Some(_)) => {
            return Err(snippet_ctx.attach_snippet(
                Error::multiple_documents("use read or read_with_options to obtain the iterator")
                    .with_location(src.last_location()),
                &src,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if src.seen_doc_end() {
                // Trailing garbage after a proper document end marker is ignored.
            } else {
                return Err(snippet_ctx.attach_snippet(e, &src));
            }
        }
    }

    if let Err(e) = src.finish() {
        return Err(snippet_ctx.attach_snippet(e, &src));
    }
    Ok(value)
}

/// Create an iterator over YAML documents from any `std::io::Read` using default options.
///
/// This is a convenience wrapper around [`read_with_options`] that uses the
/// same defaults as [`Options::default`] **except** it disables the
/// `max_reader_input_bytes` budget to better support long-lived streams.
///
/// - It streams the reader without loading the whole input into memory.
/// - Each item produced by the returned iterator is one deserialized YAML document of type `T`.
/// - Documents that are completely empty or null-like (e.g., `"", ~, null`) are skipped.
///
/// Generic parameters
/// - `R`: the concrete reader type implementing [`std::io::Read`]. You almost never need to
///   write this explicitly; the compiler will infer it from the `reader` you pass. When using
///   turbofish, write `_` to let the compiler infer `R`.
/// - `T`: the type to deserialize each YAML document into. Must implement [`serde::de::DeserializeOwned`].
///
/// Lifetimes
/// - `'a`: the lifetime of the returned iterator, tied to the lifetime of the provided `reader`.
///   The iterator cannot outlive the reader it was created from.
///
/// Limits and budget
/// - Uses the same limits as `Options::default()` (events, nodes, nesting depth, total scalar
///   bytes) and the default alias-replay caps. The only change is that
///   `Budget::max_reader_input_bytes` is set to `None` so the streaming iterator can handle
///   arbitrarily long inputs. To customize these limits, call [`read_with_options`] and set
///   `Options::budget.max_reader_input_bytes` in the provided `Options`.
/// - Alias replay limits are also enforced with their default values to mitigate alias bombs.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Simple { id: usize }
///
/// let yaml = b"id: 1\n---\nid: 2\n";
/// let mut reader = std::io::Cursor::new(&yaml[..]);
///
/// // Type `T` is inferred from the collection target (Vec<Simple>).
/// let values: Vec<Simple> = serde_saphyr::read(&mut reader)
///     .map(|r| r.unwrap())
///     .collect();
/// assert_eq!(values.len(), 2);
/// assert_eq!(values[0].id, 1);
/// ```
///
/// Specifying only `T` with turbofish and letting `R` be inferred using `_`:
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Simple { id: usize }
///
/// let yaml = b"id: 10\n---\nid: 20\n";
/// let mut reader = std::io::Cursor::new(&yaml[..]);
///
/// // First turbofish parameter is R (reader type), `_` lets the compiler infer it.
/// let iter = serde_saphyr::read::<_, Simple>(&mut reader);
/// let ids: Vec<usize> = iter.map(|res| res.unwrap().id).collect();
/// assert_eq!(ids, vec![10, 20]);
/// ```
///
/// - Each `next()` yields either `Ok(T)` for a successfully deserialized document or `Err(Error)`
///   if parsing fails or a limit is exceeded. After an error, the iterator ends.
/// - Empty/null-like documents are skipped and produce no items.
///
/// *Note* Some content of the next document is read before the current parsed document is emitted.
/// Hence, while streaming is good for safely parsing large files with multiple documents without
/// loading it into RAM in advance, it does not emit each document exactly
/// after `---`  is encountered.
pub fn read<'a, R, T>(reader: &'a mut R) -> Box<dyn Iterator<Item = Result<T, Error>> + 'a>
where
    R: Read + 'a,
    T: DeserializeOwned + 'a,
{
    Box::new(read_with_options(
        reader,
        crate::options! {
            budget: crate::budget! {
                max_reader_input_bytes: None,
            },
        },
    ))
}

/// Create an iterator over YAML documents from any `std::io::Read`, with configurable options.
///
/// This is the multi-document counterpart to [`from_reader_with_options`]. It does not load
/// the entire input into memory. Instead, it streams the reader, deserializing one document
/// at a time into values of type `T`, yielding them through the returned iterator. Documents
/// that are completely empty or null-like (e.g., `""`, `~`, or `null`) are skipped.
///
/// Generic parameters
/// - `R`: the concrete reader type that implements [`std::io::Read`]. You rarely need to spell
///   this out; it is almost always inferred from the `reader` value you pass in. When using
///   turbofish, you can write `_` for this parameter to let the compiler infer it.
/// - `T`: the type to deserialize each YAML document into. This must implement [`serde::de::DeserializeOwned`].
///
/// Lifetimes
/// - `'a`: the lifetime of the returned iterator. It is tied to the lifetime of the provided
///   `reader` value because the iterator borrows internal state that references the reader.
///   In practice, this means the iterator cannot outlive the reader it was created from.
///
/// Limits and budget
/// - All parsing limits configured via [`Options::budget`] (such as maximum events, nodes,
///   nesting depth, total scalar bytes) are enforced while streaming. from_slice_multiple hard input-byte cap
///   is also enforced via `Budget::max_reader_input_bytes` (256 MiB by default), set this
///   to None if you need a streamer to exist for arbitrary long time.
/// - Alias replay limits from [`Options::alias_limits`] are also enforced to mitigate alias bombs.
///
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Simple { id: usize }
///
/// let yaml = b"id: 1\n---\nid: 2\n";
/// let mut reader = std::io::Cursor::new(&yaml[..]);
///
/// // Type `T` is inferred from the collection target (Vec<Simple>).
/// let values: Vec<Simple> = serde_saphyr::read(&mut reader)
///     .map(|r| r.unwrap())
///     .collect();
/// assert_eq!(values.len(), 2);
/// assert_eq!(values[0].id, 1);
/// ```
///
/// Specifying only `T` with turbofish and letting `R` be inferred using `_`:
/// ```rust
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, PartialEq)]
/// struct Simple { id: usize }
///
/// let yaml = b"id: 10\n---\nid: 20\n";
/// let mut reader = std::io::Cursor::new(&yaml[..]);
///
/// // First turbofish parameter is R (reader type) which we let the compiler infer via `_`.
/// let iter = serde_saphyr::read_with_options::<_, Simple>(&mut reader, serde_saphyr::Options::default());
/// let ids: Vec<usize> = iter.map(|res| res.unwrap().id).collect();
/// assert_eq!(ids, vec![10, 20]);
/// ```
///
/// - Each `next()` yields either `Ok(T)` for a successfully deserialized document or `Err(Error)`
///   if parsing or deserialization fails.
/// - After a **deserialization error** (e.g., type mismatch, missing field), the iterator
///   automatically recovers by skipping to the next document boundary (`---`) and continues
///   iteration. This allows processing subsequent valid documents even when some fail.
/// - After a **syntax error** or **budget/alias limit exceeded**, the iterator ends because
///   the parser state may be unrecoverable.
/// - Empty/null-like documents are skipped and produce no items.
#[allow(deprecated)]
pub fn read_with_options<'a, R, T>(
    reader: &'a mut R, // iterator must not outlive this borrow
    options: Options,
) -> impl Iterator<Item = Result<T, Error>> + 'a
where
    R: Read + 'a,
    T: DeserializeOwned + 'a,
{
    struct ReadIter<'a, T> {
        src: LiveEvents<'a>, // borrows from `reader`
        cfg: crate::de::Cfg,
        finished: bool,
        _marker: std::marker::PhantomData<T>,
    }

    impl<'a, T> Iterator for ReadIter<'a, T>
    where
        T: DeserializeOwned + 'a,
    {
        type Item = Result<T, Error>;

        fn next(&mut self) -> Option<Self::Item> {
            if self.finished {
                return None;
            }
            loop {
                match self.src.peek() {
                    Ok(Some(Ev::Scalar { value, style, .. }))
                        if scalar_is_nullish(value, style) =>
                    {
                        let _ = self.src.next();
                        continue;
                    }
                    Ok(Some(_)) => {
                        let res = crate::anchor_store::with_document_scope(|| {
                            with_interp_redaction_scope(|| {
                                crate::de::with_root_redaction(
                                    crate::de::YamlDeserializer::new(&mut self.src, self.cfg),
                                    |de| T::deserialize(de),
                                )
                            })
                        });
                        if res.is_err() {
                            // After a deserialization error, skip remaining events in the
                            // current document and try to recover at the next document boundary.
                            // If no next document is found, mark as finished.
                            if !self.src.skip_to_next_document() {
                                self.finished = true;
                            }
                        }
                        return Some(res);
                    }
                    Ok(None) => {
                        self.finished = true;
                        if let Err(e) = self.src.finish() {
                            return Some(Err(e));
                        }
                        return None;
                    }
                    Err(e) => {
                        self.finished = true;
                        let _ = self.src.finish();
                        return Some(Err(e));
                    }
                }
            }
        }
    }

    let cfg = crate::de::Cfg::from_options(&options);
    let src = LiveEvents::from_reader(reader, options, false, EnforcingPolicy::PerDocument);

    ReadIter::<T> {
        src,
        cfg,
        finished: false,
        _marker: std::marker::PhantomData,
    }
}
