Skip to main content

noyalib/
de.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! YAML Deserialization.
5//!
6//! # Examples
7//!
8//! ```
9//! use noyalib::from_str;
10//! use std::collections::BTreeMap;
11//! let m: BTreeMap<String, i32> = from_str("a: 1\nb: 2\n").unwrap();
12//! assert_eq!(m.get("a"), Some(&1));
13//! ```
14
15use crate::error::{Error, Result};
16use crate::parser::{self};
17use crate::prelude::*;
18use crate::span_context;
19use crate::value::{Number, Value};
20use serde::Deserialize;
21use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
22#[cfg(feature = "std")]
23use std::io;
24
25/// Which version of the YAML specification the resolver follows.
26///
27/// YAML 1.2 (the default) and 1.1 differ in their plain-scalar
28/// resolution table:
29///
30/// | Form | 1.2 (core schema) | 1.1 |
31/// |---|---|---|
32/// | `yes` / `no` / `on` / `off` | string | bool |
33/// | `0644` | int 644 (decimal) | int 420 (octal) |
34/// | `60:00` | string | int 3 600 (sexagesimal) |
35/// | `.nan` / `.inf` | float | float (same) |
36/// | `true` / `false` | bool | bool (same) |
37///
38/// Selecting a version is a preset over the three `legacy_*` flags;
39/// see [`ParserConfig::version`] for the full mapping.
40///
41/// # Examples
42///
43/// ```
44/// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
45///
46/// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
47/// let v: Value = from_str_with_config("yes", &cfg).unwrap();
48/// assert_eq!(v, Value::Bool(true));
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51#[non_exhaustive]
52pub enum YamlVersion {
53    /// YAML 1.2 (2009) core schema. Default. Strict `true` / `false`
54    /// booleans; no bare octal; no sexagesimal.
55    #[default]
56    V1_2,
57    /// YAML 1.1 (2005). Broad resolver: `yes` / `no` / `on` / `off`
58    /// are booleans; `0644` is octal; `60:00` is sexagesimal.
59    V1_1,
60}
61
62/// Deserialization configuration.
63///
64/// All fields are public, but the struct is annotated
65/// [`#[non_exhaustive]`][nex] so that adding a new budget or
66/// policy in a future minor release is **not** a breaking change.
67/// Construct with [`ParserConfig::new`] / [`ParserConfig::strict`]
68/// / [`ParserConfig::default`] (preferred) or with the
69/// `..ParserConfig::default()` struct-update form; do not
70/// construct from an exhaustive struct-literal outside this
71/// crate.
72///
73/// [nex]: https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute
74///
75/// # Examples
76///
77/// ```
78/// use noyalib::ParserConfig;
79/// let cfg = ParserConfig::new().max_depth(64);
80/// assert_eq!(cfg.max_depth, 64);
81/// ```
82#[derive(Debug, Clone)]
83#[non_exhaustive]
84pub struct ParserConfig {
85    /// Which YAML specification version to honour during plain-scalar
86    /// resolution.
87    ///
88    /// YAML 1.2 (default) follows the **core schema** — strict
89    /// `true`/`false` booleans, no bare `0`-prefix octal, no
90    /// sexagesimal `60:00` integers. YAML 1.1 broadens the resolver
91    /// to accept all of those legacy forms.
92    ///
93    /// Setting this to [`YamlVersion::V1_1`] is equivalent to flipping
94    /// every `legacy_*` flag (`legacy_booleans`, `legacy_octal_numbers`,
95    /// `legacy_sexagesimal`) on at once. The `legacy_*` flags remain
96    /// available for fine-grained overrides — version selection sets a
97    /// preset, individual flags refine it.
98    pub yaml_version: YamlVersion,
99    /// Maximum recursion depth allowed during parsing (default: 128).
100    pub max_depth: usize,
101    /// Maximum length of a single YAML document in bytes (default: 64 MB).
102    pub max_document_length: usize,
103    /// Maximum number of times a single anchor can be expanded (default: 1024).
104    pub max_alias_expansions: usize,
105    /// Maximum number of keys allowed in a single mapping (default: 64k).
106    pub max_mapping_keys: usize,
107    /// Maximum number of elements allowed in a single sequence (default: 64k).
108    pub max_sequence_length: usize,
109    /// Maximum total parser events emitted across the input
110    /// (default: 1 000 000). Caps event-stream amplification
111    /// independent of recursion depth or alias count. Trips
112    /// [`crate::Error::Budget`] with
113    /// [`crate::BudgetBreach::MaxEvents`].
114    pub max_events: usize,
115    /// Maximum total `Value` nodes built into the AST
116    /// (default: 250 000). Trips
117    /// [`crate::BudgetBreach::MaxNodes`].
118    pub max_nodes: usize,
119    /// Maximum cumulative scalar-byte count across the document
120    /// (default: 64 MB). Distinct from
121    /// [`Self::max_document_length`] (input size) — this caps
122    /// scalar payload after alias expansion. Trips
123    /// [`crate::BudgetBreach::MaxTotalScalarBytes`].
124    pub max_total_scalar_bytes: usize,
125    /// Maximum number of documents in a multi-document stream
126    /// (default: 1 000). Trips
127    /// [`crate::BudgetBreach::MaxDocuments`].
128    pub max_documents: usize,
129    /// Maximum number of merge-key (`<<`) entries across the
130    /// document (default: 10 000). Trips
131    /// [`crate::BudgetBreach::MaxMergeKeys`].
132    pub max_merge_keys: usize,
133    /// Optional alias-to-anchor ratio heuristic for detecting
134    /// billion-laughs amplification patterns
135    /// (default: `Some(10.0)`). When more than `ratio × anchors`
136    /// aliases have been resolved, the parser trips
137    /// [`crate::BudgetBreach::AliasAnchorRatio`]. Set to `None`
138    /// to disable.
139    pub alias_anchor_ratio: Option<f64>,
140    /// How to handle duplicate keys in a mapping (default: Last, per YAML 1.2).
141    pub duplicate_key_policy: DuplicateKeyPolicy,
142    /// If true, only `true` and `false` (lowercase) are accepted as booleans.
143    pub strict_booleans: bool,
144    /// If true, accepts YAML 1.1 booleans like `yes`, `no`, `on`, `off`.
145    pub legacy_booleans: bool,
146    /// Optional registry of custom tags to strip on the streaming path.
147    ///
148    /// See [`TagRegistry`](crate::TagRegistry) for the full rationale.
149    /// `None` (default) preserves the legacy behaviour of routing every
150    /// custom-tagged value through the AST fallback.
151    pub tag_registry: Option<Arc<crate::TagRegistry>>,
152    /// How the YAML merge key (`<<`) should be handled.
153    ///
154    /// See [`MergeKeyPolicy`] for the available policies. The
155    /// default is [`MergeKeyPolicy::Auto`] — the YAML 1.2 spec
156    /// behaviour where `<<:` triggers automatic mapping merge.
157    pub merge_key_policy: MergeKeyPolicy,
158    /// When `true`, plain scalars are *never* resolved to
159    /// `null` / `bool` / `int` / `float` — every plain scalar
160    /// becomes a string. Useful for schema-strict pipelines that
161    /// require the user to quote intent explicitly. Default
162    /// `false`.
163    pub no_schema: bool,
164    /// When `true`, accept YAML 1.1-style bare `0`-prefix octal
165    /// literals (e.g. `0644` parsed as 420) in addition to the
166    /// YAML 1.2 `0o644` form. Default `false` to honour the YAML
167    /// 1.2 schema.
168    pub legacy_octal_numbers: bool,
169    /// When `true`, deserializing `!!binary "ABCD"` into a
170    /// [`String`] target yields the literal base64 source string
171    /// (`"ABCD"`) rather than rejecting on tag mismatch. The
172    /// canonical bytes path (`Vec<u8>`,
173    /// `serde_bytes::ByteBuf`) still decodes the base64 payload
174    /// either way. Useful for migrations from Python pyyaml-style
175    /// applications that treat the tag as advisory. Default
176    /// `false`.
177    pub ignore_binary_tag_for_string: bool,
178    /// When `true`, accept YAML 1.1-style **sexagesimal** numbers
179    /// (`60:00`, `1:30:00`) as integers. The colon-separated
180    /// digits are interpreted in base 60: each component is
181    /// multiplied by an increasing power of 60, summed left to
182    /// right. `60:00` → 3 600; `1:30:00` → 5 400. Negative values
183    /// (`-1:30:00`) and partial signs are honoured.
184    ///
185    /// Off by default to honour the YAML 1.2 schema. Useful for
186    /// migrations from YAML 1.1 / Ruby / pyyaml configs that use
187    /// the legacy time-of-day notation.
188    pub legacy_sexagesimal: bool,
189    /// Indentation-validation mode. See [`RequireIndent`].
190    /// Default: [`RequireIndent::Unchecked`] — accept any
191    /// well-formed YAML indent.
192    pub require_indent: RequireIndent,
193    /// Pluggable "Safe YAML" policies, run during parsing.
194    ///
195    /// Each [`Policy`](crate::policy::Policy) inspects parser
196    /// events and the post-parse [`Value`] tree; any policy
197    /// returning `Err(...)` aborts the parse with that diagnostic.
198    /// Empty by default.
199    ///
200    /// Use [`ParserConfig::with_policy`] to register a policy.
201    /// When at least one policy is present the streaming fast-path
202    /// is bypassed automatically so the policy contract holds for
203    /// every code path.
204    pub policies: Vec<Arc<dyn crate::policy::Policy>>,
205    /// `${KEY}` / `${KEY:-default}` substitution table consulted
206    /// after parsing every document.
207    ///
208    /// Each scalar in the resulting [`Value`] tree is walked and
209    /// any `${name}` placeholder is replaced with the property of
210    /// that name. Supported syntax:
211    ///
212    /// - `${name}` — substitute, error or pass through depending
213    ///   on [`Self::strict_properties`]
214    /// - `${name:-default}` — substitute, falling back to
215    ///   `default` when `name` is missing (always silent, never
216    ///   surfaces in errors)
217    /// - `${{` — literal `${` (escape for the open delimiter)
218    /// - `$$` — literal `$`
219    /// - `}}` — literal `}`
220    ///
221    /// `None` (default) disables the substitution pass entirely;
222    /// the parser is unchanged. Setting a non-empty map forces the
223    /// AST fallback so the post-parse walk runs uniformly across
224    /// every typed target.
225    #[cfg(feature = "std")]
226    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
227    pub properties: Option<Arc<std::collections::HashMap<String, String>>>,
228    /// When `true`, an unknown `${name}` placeholder (no entry in
229    /// [`Self::properties`] and no `:-default` fallback) aborts
230    /// the parse with [`Error::Custom`].
231    /// When `false` (default), unknown placeholders are replaced
232    /// with the empty string — the lossy semantics matching
233    /// [`Value::interpolate_properties_lossy`](crate::Value::interpolate_properties_lossy).
234    #[cfg(feature = "std")]
235    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
236    pub strict_properties: bool,
237    /// `!include` directive resolver. When set, the post-parse
238    /// walk substitutes every `Value::Tagged(!include, spec)`
239    /// node with the result of `resolver(IncludeRequest)`. See
240    /// [`crate::include::IncludeResolver`] for the closure
241    /// signature and [`crate::include::SafeFileResolver`] for
242    /// the bundled filesystem implementation.
243    ///
244    /// `None` (default) disables include expansion; tagged
245    /// `!include` nodes flow through unchanged.
246    #[cfg(feature = "include")]
247    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
248    pub include_resolver: Option<crate::include::IncludeResolver>,
249    /// Maximum `!include` recursion depth. Default 24. Each
250    /// nested `!include` increments the depth counter; once the
251    /// limit is reached, the parser aborts with
252    /// `Error::RecursionLimitExceeded`. Pairs with a per-walk
253    /// visited-set to catch cycles independent of depth.
254    #[cfg(feature = "include")]
255    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
256    pub max_include_depth: usize,
257}
258
259impl Default for ParserConfig {
260    fn default() -> Self {
261        ParserConfig {
262            yaml_version: YamlVersion::V1_2,
263            max_depth: 128,
264            max_document_length: 1024 * 1024 * 64, // 64 MB
265            max_alias_expansions: 1024,
266            max_mapping_keys: 1024 * 64,
267            max_sequence_length: 1024 * 64,
268            max_events: 1_000_000,
269            max_nodes: 250_000,
270            max_total_scalar_bytes: 1024 * 1024 * 64, // 64 MB
271            max_documents: 1_000,
272            max_merge_keys: 10_000,
273            alias_anchor_ratio: Some(10.0),
274            duplicate_key_policy: DuplicateKeyPolicy::default(),
275            strict_booleans: false,
276            legacy_booleans: false,
277            tag_registry: None,
278            merge_key_policy: MergeKeyPolicy::default(),
279            no_schema: false,
280            legacy_octal_numbers: false,
281            ignore_binary_tag_for_string: false,
282            legacy_sexagesimal: false,
283            require_indent: RequireIndent::Unchecked,
284            policies: Vec::new(),
285            #[cfg(feature = "std")]
286            properties: None,
287            #[cfg(feature = "std")]
288            strict_properties: false,
289            #[cfg(feature = "include")]
290            include_resolver: None,
291            #[cfg(feature = "include")]
292            max_include_depth: 24,
293        }
294    }
295}
296
297impl ParserConfig {
298    /// Create a new configuration with default values.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use noyalib::ParserConfig;
304    /// let cfg = ParserConfig::new();
305    /// assert_eq!(cfg.max_depth, 128);
306    /// ```
307    #[must_use]
308    pub fn new() -> Self {
309        Self::default()
310    }
311
312    /// Create a strict configuration (YAML 1.2 strict) with tighter
313    /// security limits suitable for untrusted input.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use noyalib::ParserConfig;
319    /// let cfg = ParserConfig::strict();
320    /// assert_eq!(cfg.max_depth, 64);
321    /// ```
322    #[must_use]
323    pub fn strict() -> Self {
324        ParserConfig {
325            yaml_version: YamlVersion::V1_2,
326            max_depth: 64,
327            max_document_length: 1024 * 1024, // 1 MB
328            max_alias_expansions: 100,
329            max_mapping_keys: 1024,
330            max_sequence_length: 1024,
331            max_events: 100_000,
332            max_nodes: 25_000,
333            max_total_scalar_bytes: 1024 * 1024, // 1 MB
334            max_documents: 100,
335            max_merge_keys: 1_000,
336            alias_anchor_ratio: Some(5.0),
337            strict_booleans: true,
338            legacy_booleans: false,
339            duplicate_key_policy: DuplicateKeyPolicy::Error,
340            tag_registry: None,
341            merge_key_policy: MergeKeyPolicy::default(),
342            no_schema: false,
343            legacy_octal_numbers: false,
344            ignore_binary_tag_for_string: false,
345            legacy_sexagesimal: false,
346            require_indent: RequireIndent::Even,
347            policies: Vec::new(),
348            #[cfg(feature = "std")]
349            properties: None,
350            #[cfg(feature = "std")]
351            strict_properties: true,
352            #[cfg(feature = "include")]
353            include_resolver: None,
354            // Strict mode tightens the include recursion ceiling
355            // proportionally to its other depth caps (max_depth
356            // 128 → 64, max_alias_expansions 1024 → 100).
357            #[cfg(feature = "include")]
358            max_include_depth: 8,
359        }
360    }
361
362    /// Install a `${KEY}` substitution table consulted after
363    /// parsing.
364    ///
365    /// Each scalar in the resulting [`Value`] tree is walked and
366    /// any `${name}` placeholder is replaced with the property of
367    /// that name. Pairs with [`Self::strict_properties`] to choose
368    /// between erroring or silently empty-substituting on unknown
369    /// keys, and with `${name:-default}` syntax for inline
370    /// defaults.
371    ///
372    /// # Examples
373    ///
374    /// ```
375    /// use noyalib::{from_str_with_config, ParserConfig, Value};
376    /// use std::collections::HashMap;
377    /// use std::sync::Arc;
378    ///
379    /// let mut props = HashMap::new();
380    /// props.insert("HOST".to_string(), "localhost".to_string());
381    /// let cfg = ParserConfig::new().properties(Arc::new(props));
382    /// let v: Value = from_str_with_config("url: http://${HOST}/", &cfg).unwrap();
383    /// assert_eq!(v["url"].as_str(), Some("http://localhost/"));
384    /// ```
385    #[cfg(feature = "std")]
386    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
387    #[must_use]
388    pub fn properties(
389        mut self,
390        properties: Arc<std::collections::HashMap<String, String>>,
391    ) -> Self {
392        self.properties = Some(properties);
393        self
394    }
395
396    /// Toggle strict-mode placeholder resolution.
397    ///
398    /// When `true`, an unknown `${name}` (no map entry, no
399    /// `:-default` fallback) aborts the parse. When `false`
400    /// (default), unknown placeholders are replaced with the empty
401    /// string — useful for environment-style configs where missing
402    /// variables should silently degrade.
403    ///
404    /// # Examples
405    ///
406    /// ```
407    /// use noyalib::{from_str_with_config, ParserConfig, Value};
408    /// use std::collections::HashMap;
409    /// use std::sync::Arc;
410    ///
411    /// let cfg = ParserConfig::new()
412    ///     .properties(Arc::new(HashMap::new()))
413    ///     .strict_properties(true);
414    /// let res: Result<Value, _> = from_str_with_config("x: ${MISSING}", &cfg);
415    /// assert!(res.is_err());
416    /// ```
417    #[cfg(feature = "std")]
418    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
419    #[must_use]
420    pub fn strict_properties(mut self, strict: bool) -> Self {
421        self.strict_properties = strict;
422        self
423    }
424
425    /// Install an `!include` directive resolver.
426    ///
427    /// Each `Value::Tagged(!include, scalar_spec)` node in the
428    /// parsed tree is replaced with the resolver's output. The
429    /// resolver is consulted with an `IncludeRequest` carrying
430    /// the verbatim spec text, a stable source-id, and the
431    /// current recursion depth.
432    ///
433    /// Pair with [`Self::max_include_depth`] to bound the
434    /// recursion ceiling. Cycle detection (A includes B includes
435    /// A) runs independently using a per-walk visited set.
436    ///
437    /// # Examples
438    ///
439    /// ```
440    /// use noyalib::include::{IncludeRequest, IncludeResolver, InputSource};
441    /// use noyalib::{ParserConfig, Result};
442    ///
443    /// let resolver = IncludeResolver::new(|req: IncludeRequest<'_>| -> Result<InputSource> {
444    ///     // For an in-memory test, fabricate a YAML payload
445    ///     // keyed on the spec.
446    ///     Ok(InputSource::new(req.spec, format!("name: {}\n", req.spec)))
447    /// });
448    /// let cfg = ParserConfig::new().include_resolver(resolver);
449    /// # let _ = cfg;
450    /// ```
451    #[cfg(feature = "include")]
452    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
453    #[must_use]
454    pub fn include_resolver(mut self, resolver: crate::include::IncludeResolver) -> Self {
455        self.include_resolver = Some(resolver);
456        self
457    }
458
459    /// Maximum `!include` recursion depth.
460    ///
461    /// Default 24 (8 in [`Self::strict()`]). Each nested
462    /// `!include` increments the depth; once the limit is
463    /// reached, the parser aborts with
464    /// `Error::RecursionLimitExceeded`. The cap is independent
465    /// of [`Self::max_depth`] (which bounds *YAML structural*
466    /// nesting) and of the per-walk cycle-detection set (which
467    /// catches A→B→A regardless of depth).
468    #[cfg(feature = "include")]
469    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
470    #[must_use]
471    pub fn max_include_depth(mut self, depth: usize) -> Self {
472        self.max_include_depth = depth;
473        self
474    }
475
476    /// Select the YAML specification version the resolver should
477    /// honour.
478    ///
479    /// Selecting [`YamlVersion::V1_1`] is a *preset* over the three
480    /// `legacy_*` flags — equivalent to:
481    ///
482    /// ```text
483    /// cfg.legacy_booleans      = true;  // yes / no / on / off
484    /// cfg.legacy_octal_numbers = true;  // 0644 → octal
485    /// cfg.legacy_sexagesimal   = true;  // 60:00 → 3600
486    /// ```
487    ///
488    /// Selecting [`YamlVersion::V1_2`] resets those three flags to
489    /// `false` so callers can revert to strict 1.2 mode without
490    /// re-creating the config from scratch. Other fields (limits,
491    /// policies, merge-key behaviour) are unaffected.
492    ///
493    /// Fine-grained overrides (e.g. "1.1 booleans but reject octal
494    /// `0644`") work as expected: call `version` first, then flip
495    /// individual flags.
496    ///
497    /// # Examples
498    ///
499    /// ```
500    /// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
501    ///
502    /// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
503    /// // YAML 1.1 booleans
504    /// let v: Value = from_str_with_config("on", &cfg).unwrap();
505    /// assert_eq!(v, Value::Bool(true));
506    /// // YAML 1.1 octal
507    /// let v: Value = from_str_with_config("0644", &cfg).unwrap();
508    /// assert_eq!(v, Value::from(420_i64));
509    /// // YAML 1.1 sexagesimal
510    /// let v: Value = from_str_with_config("1:30", &cfg).unwrap();
511    /// assert_eq!(v, Value::from(90_i64));
512    /// ```
513    #[must_use]
514    pub fn version(mut self, version: YamlVersion) -> Self {
515        self.yaml_version = version;
516        match version {
517            YamlVersion::V1_1 => {
518                self.legacy_booleans = true;
519                self.legacy_octal_numbers = true;
520                self.legacy_sexagesimal = true;
521            }
522            YamlVersion::V1_2 => {
523                self.legacy_booleans = false;
524                self.legacy_octal_numbers = false;
525                self.legacy_sexagesimal = false;
526            }
527        }
528        self
529    }
530
531    /// Register a [`Policy`](crate::policy::Policy) to enforce
532    /// during parsing.
533    ///
534    /// Multiple policies may be registered; they all run in
535    /// registration order, and the first error short-circuits the
536    /// parse. When any policy is present the streaming fast-path
537    /// is bypassed so the policy contract is enforced uniformly.
538    ///
539    /// # Examples
540    ///
541    /// ```
542    /// use noyalib::{from_str_with_config, ParserConfig, Value};
543    /// use noyalib::policy::DenyAnchors;
544    ///
545    /// let cfg = ParserConfig::new().with_policy(DenyAnchors);
546    /// let res: Result<Value, _> =
547    ///     from_str_with_config("a: &x 1\nb: *x\n", &cfg);
548    /// assert!(res.is_err());
549    /// ```
550    #[must_use]
551    pub fn with_policy<P>(mut self, policy: P) -> Self
552    where
553        P: crate::policy::Policy + 'static,
554    {
555        self.policies.push(Arc::new(policy));
556        self
557    }
558
559    /// Set the maximum recursion depth.
560    ///
561    /// # Examples
562    ///
563    /// ```
564    /// use noyalib::ParserConfig;
565    /// let cfg = ParserConfig::new().max_depth(32);
566    /// assert_eq!(cfg.max_depth, 32);
567    /// ```
568    #[must_use]
569    pub fn max_depth(mut self, depth: usize) -> Self {
570        self.max_depth = depth;
571        self
572    }
573
574    /// Set the maximum document length.
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// use noyalib::ParserConfig;
580    /// let cfg = ParserConfig::new().max_document_length(1024);
581    /// assert_eq!(cfg.max_document_length, 1024);
582    /// ```
583    #[must_use]
584    pub fn max_document_length(mut self, len: usize) -> Self {
585        self.max_document_length = len;
586        self
587    }
588
589    /// Set the maximum alias expansions.
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// use noyalib::ParserConfig;
595    /// let cfg = ParserConfig::new().max_alias_expansions(50);
596    /// assert_eq!(cfg.max_alias_expansions, 50);
597    /// ```
598    #[must_use]
599    pub fn max_alias_expansions(mut self, expansions: usize) -> Self {
600        self.max_alias_expansions = expansions;
601        self
602    }
603
604    /// Set the maximum number of mapping keys.
605    ///
606    /// # Examples
607    ///
608    /// ```
609    /// use noyalib::ParserConfig;
610    /// let cfg = ParserConfig::new().max_mapping_keys(100);
611    /// assert_eq!(cfg.max_mapping_keys, 100);
612    /// ```
613    #[must_use]
614    pub fn max_mapping_keys(mut self, max: usize) -> Self {
615        self.max_mapping_keys = max;
616        self
617    }
618
619    /// Set the maximum sequence length.
620    ///
621    /// # Examples
622    ///
623    /// ```
624    /// use noyalib::ParserConfig;
625    /// let cfg = ParserConfig::new().max_sequence_length(100);
626    /// assert_eq!(cfg.max_sequence_length, 100);
627    /// ```
628    #[must_use]
629    pub fn max_sequence_length(mut self, max: usize) -> Self {
630        self.max_sequence_length = max;
631        self
632    }
633
634    /// Set the maximum total parser-event budget.
635    ///
636    /// # Examples
637    ///
638    /// ```
639    /// use noyalib::ParserConfig;
640    /// let cfg = ParserConfig::new().max_events(50_000);
641    /// assert_eq!(cfg.max_events, 50_000);
642    /// ```
643    #[must_use]
644    pub fn max_events(mut self, max: usize) -> Self {
645        self.max_events = max;
646        self
647    }
648
649    /// Set the maximum total `Value` node budget.
650    ///
651    /// # Examples
652    ///
653    /// ```
654    /// use noyalib::ParserConfig;
655    /// let cfg = ParserConfig::new().max_nodes(10_000);
656    /// assert_eq!(cfg.max_nodes, 10_000);
657    /// ```
658    #[must_use]
659    pub fn max_nodes(mut self, max: usize) -> Self {
660        self.max_nodes = max;
661        self
662    }
663
664    /// Set the maximum cumulative scalar-byte budget.
665    ///
666    /// Distinct from [`Self::max_document_length`] — this caps
667    /// scalar bytes after alias expansion.
668    ///
669    /// # Examples
670    ///
671    /// ```
672    /// use noyalib::ParserConfig;
673    /// let cfg = ParserConfig::new().max_total_scalar_bytes(8 * 1024 * 1024);
674    /// assert_eq!(cfg.max_total_scalar_bytes, 8 * 1024 * 1024);
675    /// ```
676    #[must_use]
677    pub fn max_total_scalar_bytes(mut self, max: usize) -> Self {
678        self.max_total_scalar_bytes = max;
679        self
680    }
681
682    /// Set the maximum document count for multi-document streams.
683    ///
684    /// # Examples
685    ///
686    /// ```
687    /// use noyalib::ParserConfig;
688    /// let cfg = ParserConfig::new().max_documents(64);
689    /// assert_eq!(cfg.max_documents, 64);
690    /// ```
691    #[must_use]
692    pub fn max_documents(mut self, max: usize) -> Self {
693        self.max_documents = max;
694        self
695    }
696
697    /// Set the maximum merge-key count budget.
698    ///
699    /// # Examples
700    ///
701    /// ```
702    /// use noyalib::ParserConfig;
703    /// let cfg = ParserConfig::new().max_merge_keys(1_000);
704    /// assert_eq!(cfg.max_merge_keys, 1_000);
705    /// ```
706    #[must_use]
707    pub fn max_merge_keys(mut self, max: usize) -> Self {
708        self.max_merge_keys = max;
709        self
710    }
711
712    /// Set the indentation-validation mode.
713    ///
714    /// # Examples
715    ///
716    /// ```
717    /// use noyalib::{ParserConfig, RequireIndent};
718    /// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
719    /// assert_eq!(cfg.require_indent, RequireIndent::Even);
720    /// ```
721    #[must_use]
722    pub fn require_indent(mut self, mode: RequireIndent) -> Self {
723        self.require_indent = mode;
724        self
725    }
726
727    /// Set the alias-to-anchor ratio heuristic.
728    ///
729    /// Pass `Some(ratio)` to enable the billion-laughs guard,
730    /// `None` to disable.
731    ///
732    /// # Examples
733    ///
734    /// ```
735    /// use noyalib::ParserConfig;
736    /// let cfg = ParserConfig::new().alias_anchor_ratio(Some(20.0));
737    /// assert_eq!(cfg.alias_anchor_ratio, Some(20.0));
738    /// ```
739    #[must_use]
740    pub fn alias_anchor_ratio(mut self, ratio: Option<f64>) -> Self {
741        self.alias_anchor_ratio = ratio;
742        self
743    }
744
745    /// Set the duplicate key policy.
746    ///
747    /// # Examples
748    ///
749    /// ```
750    /// use noyalib::{DuplicateKeyPolicy, ParserConfig};
751    /// let cfg = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
752    /// assert_eq!(cfg.duplicate_key_policy, DuplicateKeyPolicy::Error);
753    /// ```
754    #[must_use]
755    pub fn duplicate_key_policy(mut self, policy: DuplicateKeyPolicy) -> Self {
756        self.duplicate_key_policy = policy;
757        self
758    }
759
760    /// Enable or disable strict booleans.
761    ///
762    /// # Examples
763    ///
764    /// ```
765    /// use noyalib::ParserConfig;
766    /// let cfg = ParserConfig::new().strict_booleans(true);
767    /// assert!(cfg.strict_booleans);
768    /// ```
769    #[must_use]
770    pub fn strict_booleans(mut self, strict: bool) -> Self {
771        self.strict_booleans = strict;
772        self
773    }
774
775    /// Enable or disable legacy booleans.
776    ///
777    /// # Examples
778    ///
779    /// ```
780    /// use noyalib::ParserConfig;
781    /// let cfg = ParserConfig::new().legacy_booleans(true);
782    /// assert!(cfg.legacy_booleans);
783    /// ```
784    #[must_use]
785    pub fn legacy_booleans(mut self, legacy: bool) -> Self {
786        self.legacy_booleans = legacy;
787        self
788    }
789
790    /// Attach a [`TagRegistry`](crate::TagRegistry) so the streaming
791    /// deserializer strips listed custom tags instead of routing them
792    /// through the AST.
793    ///
794    /// See the [`tag_registry`](crate::tag_registry) module
795    /// documentation for when to use this versus `#[serde(rename)]`.
796    ///
797    /// # Examples
798    ///
799    /// ```
800    /// use noyalib::{ParserConfig, TagRegistry};
801    /// use std::sync::Arc;
802    /// let reg = Arc::new(TagRegistry::new().with("!Celsius"));
803    /// let cfg = ParserConfig::new().tag_registry(Arc::clone(&reg));
804    /// assert!(cfg.tag_registry.is_some());
805    /// ```
806    #[must_use]
807    pub fn tag_registry(mut self, registry: Arc<crate::TagRegistry>) -> Self {
808        self.tag_registry = Some(registry);
809        self
810    }
811
812    /// Set the policy for handling the YAML merge key (`<<`).
813    ///
814    /// # Examples
815    ///
816    /// ```
817    /// use noyalib::{MergeKeyPolicy, ParserConfig};
818    /// let cfg = ParserConfig::new().merge_key_policy(MergeKeyPolicy::AsOrdinary);
819    /// assert_eq!(cfg.merge_key_policy, MergeKeyPolicy::AsOrdinary);
820    /// ```
821    #[must_use]
822    pub fn merge_key_policy(mut self, policy: MergeKeyPolicy) -> Self {
823        self.merge_key_policy = policy;
824        self
825    }
826
827    /// Toggle schema-free plain-scalar resolution. When `true`,
828    /// every plain scalar becomes a string regardless of whether
829    /// it would normally resolve to `null`, `bool`, integer, or
830    /// float.
831    ///
832    /// # Examples
833    ///
834    /// ```
835    /// use noyalib::ParserConfig;
836    /// let cfg = ParserConfig::new().no_schema(true);
837    /// assert!(cfg.no_schema);
838    /// ```
839    #[must_use]
840    pub fn no_schema(mut self, no_schema: bool) -> Self {
841        self.no_schema = no_schema;
842        self
843    }
844
845    /// Toggle YAML 1.1-style bare `0`-prefix octal parsing
846    /// (e.g. `0644` → 420). Off by default; YAML 1.2 requires the
847    /// `0o` prefix.
848    ///
849    /// # Examples
850    ///
851    /// ```
852    /// use noyalib::ParserConfig;
853    /// let cfg = ParserConfig::new().legacy_octal_numbers(true);
854    /// assert!(cfg.legacy_octal_numbers);
855    /// ```
856    #[must_use]
857    pub fn legacy_octal_numbers(mut self, on: bool) -> Self {
858        self.legacy_octal_numbers = on;
859        self
860    }
861
862    /// Toggle the migration-helper behaviour where
863    /// `!!binary "ABCD"` deserializes into a [`String`] target as
864    /// the literal base64 source string. The canonical bytes
865    /// path (`Vec<u8>`, `serde_bytes::ByteBuf`) is unaffected —
866    /// it always decodes the base64 payload.
867    ///
868    /// # Examples
869    ///
870    /// ```
871    /// use noyalib::ParserConfig;
872    /// let cfg = ParserConfig::new().ignore_binary_tag_for_string(true);
873    /// assert!(cfg.ignore_binary_tag_for_string);
874    /// ```
875    #[must_use]
876    pub fn ignore_binary_tag_for_string(mut self, on: bool) -> Self {
877        self.ignore_binary_tag_for_string = on;
878        self
879    }
880
881    /// Toggle YAML 1.1-style sexagesimal number parsing
882    /// (`60:00` → 3 600). Off by default; YAML 1.2 dropped the
883    /// sexagesimal schema, so plain `1:30:00` would otherwise
884    /// surface as a string.
885    ///
886    /// # Examples
887    ///
888    /// ```
889    /// use noyalib::ParserConfig;
890    /// let cfg = ParserConfig::new().legacy_sexagesimal(true);
891    /// assert!(cfg.legacy_sexagesimal);
892    /// ```
893    #[must_use]
894    pub fn legacy_sexagesimal(mut self, on: bool) -> Self {
895        self.legacy_sexagesimal = on;
896        self
897    }
898}
899
900/// Policy for handling the YAML merge key (`<<`) during parsing.
901///
902/// YAML 1.2 §10.2 defines `<<` as a "merge key" that, when used as
903/// a mapping key, splices the value's mapping (or sequence of
904/// mappings) into the enclosing mapping. The variants below let
905/// callers opt out of that behaviour.
906///
907/// # Examples
908///
909/// Indentation-validation mode for the YAML scanner.
910///
911/// Issue #6 surface — every block-context indent transition is
912/// classified per the chosen mode. The default
913/// ([`RequireIndent::Unchecked`]) accepts any well-formed YAML
914/// indent (the YAML 1.2 spec mandate), which is what every
915/// other Rust YAML parser does.
916///
917/// Stricter modes are useful in pipelines that require uniform
918/// indentation house style (linters, formatters, reviewer
919/// gates) — a config file with mixed `2`-space and `4`-space
920/// indent passes by-spec but fails consistency review.
921///
922/// # Variants
923///
924/// - [`RequireIndent::Unchecked`] (default) — by-spec mode.
925/// - [`RequireIndent::Even`] — every indent delta must be even.
926/// - `RequireIndent::Divisible(N)` — every indent delta must
927///   be divisible by `N`.
928/// - `RequireIndent::Uniform(Some(N))` — every indent delta
929///   must equal `N`. `None` means "auto-detect from the first
930///   delta and require the rest to match it".
931///
932/// # Examples
933///
934/// ```
935/// use noyalib::{ParserConfig, RequireIndent};
936/// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
937/// assert_eq!(cfg.require_indent, RequireIndent::Even);
938/// ```
939#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
940#[non_exhaustive]
941pub enum RequireIndent {
942    /// Accept any indent transition the YAML 1.2 spec allows.
943    /// Default.
944    #[default]
945    Unchecked,
946    /// Indent delta must be even (`2`, `4`, `6`, …). The most
947    /// common house-style.
948    Even,
949    /// Indent delta must be divisible by `N`.
950    Divisible(usize),
951    /// `Some(N)`: every indent delta must equal `N`.
952    /// `None`: the first delta sets the standard for the
953    /// document; subsequent deltas must match it.
954    Uniform(Option<usize>),
955}
956
957/// ```
958/// use noyalib::MergeKeyPolicy;
959/// assert_eq!(MergeKeyPolicy::default(), MergeKeyPolicy::Auto);
960/// ```
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
962#[non_exhaustive]
963pub enum MergeKeyPolicy {
964    /// Apply the YAML 1.2 merge-key semantics — `<<:` keys trigger
965    /// automatic merge of the value into the enclosing mapping.
966    /// Default.
967    #[default]
968    Auto,
969    /// Treat `<<` as an ordinary string key. The mapping retains a
970    /// literal `<<` entry whose value is whatever the YAML
971    /// document supplied. Useful when round-tripping configuration
972    /// files that happen to contain a `<<` key for non-merge
973    /// reasons.
974    AsOrdinary,
975    /// Reject any document that contains a `<<` key with
976    /// [`crate::Error::Custom`]. Useful for schema-strict pipelines
977    /// where merge keys are forbidden.
978    Error,
979}
980
981/// Policy for handling duplicate keys in a YAML mapping.
982///
983/// # Examples
984///
985/// ```
986/// use noyalib::DuplicateKeyPolicy;
987/// assert_eq!(DuplicateKeyPolicy::default(), DuplicateKeyPolicy::Last);
988/// ```
989#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
990#[non_exhaustive]
991pub enum DuplicateKeyPolicy {
992    /// Use the first occurrence of the key; ignore subsequent ones.
993    First,
994    /// Use the last occurrence of the key (YAML 1.2 default).
995    #[default]
996    Last,
997    /// Return an error if a duplicate key is encountered.
998    Error,
999}
1000
1001/// Deserialize YAML from a `&str` into a typed `T`.
1002///
1003/// Default entry point for typed deserialisation. Drives the
1004/// streaming fast-path when the input matches its eligibility
1005/// rules (no custom merge-key policy, no ignore-binary-tag mode,
1006/// no registered policies); otherwise routes through the
1007/// `Value`-AST loader. The choice is transparent — both paths
1008/// produce identical results.
1009///
1010/// # Errors
1011///
1012/// Returns [`Error`](crate::Error) when:
1013///
1014/// - `Error::Parse` / `Error::ParseWithLocation` — `s` is not
1015///   well-formed YAML 1.2 (missing closing bracket, indentation
1016///   mismatch, invalid escape, …).
1017/// - `Error::Deserialize` — the document parses but does not
1018///   match `T`'s shape (wrong scalar type, missing required field
1019///   on a struct without `#[serde(default)]`, unknown enum
1020///   variant, …).
1021/// - `Error::DepthLimit` / `Error::DocumentTooLong` /
1022///   `Error::AliasLimit` — input exceeds the default
1023///   [`ParserConfig`] safety budgets. Use
1024///   [`from_str_with_config`] with [`ParserConfig::strict()`] for
1025///   tighter limits or with relaxed limits if the defaults
1026///   reject a known-good document.
1027/// - `Error::DuplicateKey` — only when `duplicate_key_policy`
1028///   has been switched to `Error`. The default policy is `Last`,
1029///   which deduplicates without erroring.
1030/// - `Error::Custom` — surface for upstream `serde::de::Error`
1031///   conversions; ordinarily the more specific variants above are
1032///   produced first.
1033///
1034/// # Examples
1035///
1036/// ```
1037/// let n: i32 = noyalib::from_str("42").unwrap();
1038/// assert_eq!(n, 42);
1039/// ```
1040pub fn from_str<T>(s: &str) -> Result<T>
1041where
1042    T: for<'de> Deserialize<'de> + 'static,
1043{
1044    from_str_with_config(s, &ParserConfig::default())
1045}
1046
1047/// Deserialise a YAML document into a target type that may borrow
1048/// from the input slice (e.g. `&'a str`, `Cow<'a, str>`, structs
1049/// containing those).
1050///
1051/// Where [`from_str`] requires `T: DeserializeOwned + 'static` and
1052/// therefore can never satisfy `Deserialize<'de> for &'de str`, this
1053/// function pins the deserialiser's lifetime to the input buffer's
1054/// lifetime. Plain (unquoted) and unescaped quoted scalars are
1055/// surfaced via `visit_borrowed_str`, allowing zero-copy borrows.
1056/// Scalars that required transformation (escape decoding, line
1057/// folding, alias replay, tag resolution) fall back to owned
1058/// allocations — see [`crate::borrowed::TransformReason`] for the
1059/// catalogue of transform causes.
1060///
1061/// # Errors
1062///
1063/// Returns an error if `s` is not valid YAML, exceeds the default
1064/// security limits, or cannot be deserialised into `T`. When `T`
1065/// targets `&'a str` and the YAML scalar required transformation,
1066/// serde's `&str` visitor surfaces an `invalid type: string,
1067/// expected a borrowed string` error — switch the target to
1068/// `Cow<'a, str>` or `String` to accept either form.
1069///
1070/// # Examples
1071///
1072/// ```
1073/// use noyalib::from_str_borrowing;
1074/// use serde::Deserialize;
1075///
1076/// #[derive(Deserialize)]
1077/// struct Person<'a> {
1078///     name: &'a str,
1079///     role: &'a str,
1080/// }
1081///
1082/// let yaml = "name: noyalib\nrole: parser\n";
1083/// let p: Person<'_> = from_str_borrowing(yaml).unwrap();
1084/// assert_eq!(p.name, "noyalib");
1085/// assert_eq!(p.role, "parser");
1086/// ```
1087pub fn from_str_borrowing<'a, T>(s: &'a str) -> Result<T>
1088where
1089    T: Deserialize<'a>,
1090{
1091    from_str_borrowing_with_config(s, &ParserConfig::default())
1092}
1093
1094/// [`from_str_borrowing`] with a custom [`ParserConfig`] — typically
1095/// to tighten security limits for untrusted input.
1096///
1097/// # Errors
1098///
1099/// Returns an error if `s` is not valid YAML under the supplied
1100/// config or cannot be deserialised into `T`.
1101///
1102/// # Examples
1103///
1104/// ```
1105/// use noyalib::{from_str_borrowing_with_config, ParserConfig};
1106/// let cfg = ParserConfig::strict();
1107/// let s: &str = from_str_borrowing_with_config("hello\n", &cfg).unwrap();
1108/// assert_eq!(s, "hello");
1109/// ```
1110pub fn from_str_borrowing_with_config<'a, T>(s: &'a str, config: &ParserConfig) -> Result<T>
1111where
1112    T: Deserialize<'a>,
1113{
1114    let parse_config = parser::ParseConfig::from(config);
1115    if s.len() > parse_config.max_document_length {
1116        return Err(Error::Parse(format!(
1117            "document exceeds maximum length of {} bytes",
1118            parse_config.max_document_length
1119        )));
1120    }
1121    let mut de = crate::streaming::StreamingDeserializer::with_config(s, parse_config);
1122    if let Some(registry) = config.tag_registry.as_ref() {
1123        de = de.with_tag_registry(Arc::clone(registry));
1124    }
1125    T::deserialize(&mut de)
1126}
1127
1128/// Compile-time-ish check: is the deserialise target `T` exactly
1129/// [`Value`]? Used by [`from_str_with_config`] / [`from_value`]
1130/// to enable the tag-preserving fast-path on
1131/// [`Deserializer::deserialize_any`] only when the caller wants a
1132/// `Value`. For typed targets (`#[derive(Deserialize)] struct`,
1133/// scalars, enums, …) the standard transparent-tag behaviour
1134/// stays in place.
1135///
1136/// `Value` is `'static`, so [`std::any::TypeId::of`] is well-formed
1137/// here. The check returns `false` for any other `T`, including
1138/// `Spanned<Value>` and `Vec<Value>` (where the outer wrapper has
1139/// a distinct `TypeId`).
1140fn is_value_target<T: 'static + ?Sized>() -> bool {
1141    use core::any::TypeId;
1142    TypeId::of::<T>() == TypeId::of::<Value>()
1143}
1144
1145/// Internal typed-deserialise entry that does **not** require
1146/// `T: 'static` and never engages the tag-preserving fast-path.
1147///
1148/// Used by integrations whose external trait signatures expose
1149/// `T: for<'de> Deserialize<'de>` without a `'static` bound (e.g.
1150/// the [`figment`] crate's [`figment::Format::from_str`]
1151/// signature). In those contexts the caller has already
1152/// type-erased through `T = ProfileFigure` etc., and a tag-
1153/// preserving Value reconstitution would never apply anyway.
1154///
1155/// Mirrors [`from_str_with_config`] in every other respect:
1156/// streaming fast-path → AST loader fallback → policy walk →
1157/// `T::deserialize`.
1158#[cfg(all(feature = "std", feature = "figment"))]
1159pub(crate) fn from_str_typed_no_tag_preserve<T>(s: &str, config: &ParserConfig) -> Result<T>
1160where
1161    T: for<'de> Deserialize<'de>,
1162{
1163    let stream_eligible = config.merge_key_policy == MergeKeyPolicy::Auto
1164        && !config.ignore_binary_tag_for_string
1165        && config.policies.is_empty();
1166    if stream_eligible {
1167        if let Some(res) = crate::streaming::from_str_streaming(s, config) {
1168            return res;
1169        }
1170    }
1171    let parse_config = parser::ParseConfig::from(config);
1172    let (value, span_tree) = parser::parse_one(s, &parse_config)?;
1173    for p in &config.policies {
1174        p.check_value(&value)?;
1175    }
1176    let spans = span_context::build_span_map(&value, &span_tree);
1177    let ctx = span_context::SpanContext {
1178        spans,
1179        source: s.into(),
1180    };
1181    let _guard = span_context::set_span_context(ctx);
1182    let de = Deserializer::with_options(
1183        &value,
1184        Some(_guard.as_ref()),
1185        config.ignore_binary_tag_for_string,
1186    );
1187    T::deserialize(de)
1188}
1189
1190/// Strict deserialise: like [`from_str`] but errors if `s`
1191/// contains any keys that the target type `T` does not declare.
1192///
1193/// Solves the typo-in-config-key problem — by default, both
1194/// `serde_yaml` and noyalib silently ignore keys the struct
1195/// does not know about, so a misspelled `replicass: 3` (with
1196/// the typo) deserialises into a struct whose `replicas` field
1197/// stays at its `Default`. `from_str_strict` surfaces those
1198/// extra keys as a typed `Error::UnknownField` listing every
1199/// offending path.
1200///
1201/// # Errors
1202///
1203/// - Any key in the YAML document is not declared on `T`.
1204/// - Any of the regular [`from_str`] error paths.
1205///
1206/// # Examples
1207///
1208/// ```
1209/// use serde::Deserialize;
1210///
1211/// #[derive(Debug, Deserialize)]
1212/// struct Config {
1213///     port: u16,
1214/// }
1215///
1216/// // The typo "porrt" is silently ignored by `from_str`. With
1217/// // `from_str_strict` it surfaces as a typed error.
1218/// let yaml = "port: 8080\nporrt: 9090\n";
1219/// assert!(noyalib::from_str::<Config>(yaml).is_ok());
1220/// assert!(noyalib::from_str_strict::<Config>(yaml).is_err());
1221/// ```
1222#[cfg(all(feature = "std", feature = "strict-deserialise"))]
1223pub fn from_str_strict<T>(s: &str) -> Result<T>
1224where
1225    T: for<'de> Deserialize<'de> + 'static,
1226{
1227    let unknown = std::sync::Mutex::new(Vec::<String>::new());
1228    let value: Value = from_str_with_config(s, &ParserConfig::default())?;
1229    let result: Result<T> = serde_ignored::deserialize(&value, |path| {
1230        unknown
1231            .lock()
1232            .expect("from_str_strict: ignored-paths lock poisoned")
1233            .push(path.to_string());
1234    });
1235    let extras = unknown
1236        .into_inner()
1237        .expect("from_str_strict: ignored-paths lock poisoned");
1238    let typed = result?;
1239    if !extras.is_empty() {
1240        let msg = if extras.len() == 1 {
1241            format!("unknown field at `{}`", extras[0])
1242        } else {
1243            let joined = extras
1244                .iter()
1245                .map(|p| format!("`{p}`"))
1246                .collect::<Vec<_>>()
1247                .join(", ");
1248            format!("unknown fields: {joined}")
1249        };
1250        return Err(Error::UnknownField(msg));
1251    }
1252    Ok(typed)
1253}
1254
1255/// Strict deserialise from a byte slice — like [`from_slice`] but
1256/// errors if the input contains keys the target type `T` does not
1257/// declare.
1258///
1259/// Same semantics as [`from_str_strict`]. Use this when the caller
1260/// already holds a `&[u8]` (e.g. data read from a buffer or returned
1261/// by a `bytes` framework) and would otherwise pay the UTF-8
1262/// validation cost of converting to `&str` twice.
1263///
1264/// # Errors
1265///
1266/// - The byte slice is not valid UTF-8.
1267/// - Any key in the YAML document is not declared on `T`.
1268/// - Any of the regular [`from_slice`] error paths.
1269///
1270/// # Examples
1271///
1272/// ```
1273/// use serde::Deserialize;
1274///
1275/// #[derive(Debug, Deserialize)]
1276/// struct Config {
1277///     port: u16,
1278/// }
1279///
1280/// let yaml: &[u8] = b"port: 8080\nporrt: 9090\n";
1281/// assert!(noyalib::from_slice::<Config>(yaml).is_ok());
1282/// assert!(noyalib::from_slice_strict::<Config>(yaml).is_err());
1283/// ```
1284#[cfg(all(feature = "std", feature = "strict-deserialise"))]
1285pub fn from_slice_strict<T>(b: &[u8]) -> Result<T>
1286where
1287    T: for<'de> Deserialize<'de> + 'static,
1288{
1289    let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
1290    from_str_strict(s)
1291}
1292
1293/// Strict deserialise from an IO reader — like [`from_reader`] but
1294/// errors if the input contains keys the target type `T` does not
1295/// declare.
1296///
1297/// Same semantics as [`from_str_strict`]. Reads the entire stream
1298/// into memory before parsing, mirroring [`from_reader`].
1299///
1300/// # Errors
1301///
1302/// - The reader returns an I/O error or the data is not valid UTF-8.
1303/// - Any key in the YAML document is not declared on `T`.
1304/// - Any of the regular [`from_reader`] error paths.
1305///
1306/// # Examples
1307///
1308/// ```
1309/// use serde::Deserialize;
1310///
1311/// #[derive(Debug, Deserialize)]
1312/// struct Config {
1313///     port: u16,
1314/// }
1315///
1316/// let yaml = b"port: 8080\nporrt: 9090\n".to_vec();
1317/// assert!(noyalib::from_reader::<_, Config>(&yaml[..]).is_ok());
1318/// assert!(noyalib::from_reader_strict::<_, Config>(&yaml[..]).is_err());
1319/// ```
1320#[cfg(all(feature = "std", feature = "strict-deserialise"))]
1321pub fn from_reader_strict<R, T>(mut reader: R) -> Result<T>
1322where
1323    R: io::Read,
1324    T: for<'de> Deserialize<'de> + 'static,
1325{
1326    let mut s = String::new();
1327    let _ = reader.read_to_string(&mut s).map_err(Error::Io)?;
1328    from_str_strict(&s)
1329}
1330
1331/// Deserialize YAML from a `&str` with a custom [`ParserConfig`].
1332///
1333/// Use this when the defaults need overriding — common reasons:
1334/// untrusted input ([`ParserConfig::strict()`]), pipeline-specific
1335/// limits ([`ParserConfig::max_depth`]), YAML 1.1 compatibility
1336/// ([`ParserConfig::version`]), or custom safe-YAML
1337/// [`policy::Policy`](crate::policy::Policy) enforcement.
1338///
1339/// # Errors
1340///
1341/// Same variant set as [`from_str`]. The active `config` controls
1342/// which limit-related errors can fire:
1343///
1344/// - When `config.duplicate_key_policy == DuplicateKeyPolicy::Error`,
1345///   any duplicate mapping key returns `Error::DuplicateKey`.
1346/// - When the input exceeds the configured `max_depth`,
1347///   `max_document_length`, `max_alias_expansions`,
1348///   `max_mapping_keys`, or `max_sequence_length`, the matching
1349///   `Error::*Limit` variant is returned.
1350/// - When `config.policies` contains a policy that rejects the
1351///   document, `Error::Deserialize` is returned with the policy's
1352///   diagnostic.
1353///
1354/// # Examples
1355///
1356/// ```
1357/// use noyalib::{from_str_with_config, ParserConfig};
1358/// let cfg = ParserConfig::strict();
1359/// let n: i32 = from_str_with_config("7", &cfg).unwrap();
1360/// assert_eq!(n, 7);
1361/// ```
1362pub fn from_str_with_config<T>(s: &str, config: &ParserConfig) -> Result<T>
1363where
1364    T: for<'de> Deserialize<'de> + 'static,
1365{
1366    // Try streaming path first (faster, no intermediate Value AST).
1367    // The streaming path bakes in YAML 1.2 semantics:
1368    // - `<<: *alias` merges natively;
1369    // - `!!binary` is propagated as a typed tag.
1370    // When the caller asked for a non-default behaviour on either
1371    // axis, route through the AST loader so the relevant toggle
1372    // takes effect. Active `properties` interpolation also disables
1373    // the streaming path so the post-parse substitution walk runs.
1374    let stream_eligible = config.merge_key_policy == MergeKeyPolicy::Auto
1375        && !config.ignore_binary_tag_for_string
1376        && config.policies.is_empty()
1377        && properties_inactive(config)
1378        && includes_inactive(config);
1379    if stream_eligible {
1380        if let Some(res) = crate::streaming::from_str_streaming(s, config) {
1381            return res;
1382        }
1383    }
1384
1385    let parse_config = parser::ParseConfig::from(config);
1386
1387    // Skip-span + zero-rewalk fast path: when T == Value, the AST
1388    // we just parsed *is* the answer. The default path would
1389    // (a) build a `SpanTree` that `Value::deserialize` never
1390    // consults, then (b) hand the parsed `Value` back to serde,
1391    // which walks it a second time and rebuilds an identical
1392    // `Value` via `ValueVisitor::visit_seq` / `visit_map` — pure
1393    // waste. Instead, parse via `parse_one_value` (no SpanTree)
1394    // and downcast the `Value` directly into `T`. The downcast
1395    // is the safe stdlib `Box<dyn Any>::downcast::<T>()` and is
1396    // provably correct because `is_value_target::<T>()` already
1397    // verified `TypeId::of::<T>() == TypeId::of::<Value>()`.
1398    if is_value_target::<T>() {
1399        let mut value = parser::parse_one_value(s, &parse_config)?;
1400        apply_includes(&mut value, config)?;
1401        apply_properties(&mut value, config)?;
1402        for p in &config.policies {
1403            p.check_value(&value)?;
1404        }
1405        let boxed: Box<dyn core::any::Any> = Box::new(value);
1406        // SAFETY-by-construction: `is_value_target::<T>()` already
1407        // verified `TypeId::of::<T>() == TypeId::of::<Value>()`, so
1408        // the boxed `Value` downcasts to `T` infallibly. `.expect()`
1409        // documents the invariant; the path is provably unreachable.
1410        let downcast: Box<T> = boxed
1411            .downcast::<T>()
1412            .expect("is_value_target proved T == Value");
1413        return Ok(*downcast);
1414    }
1415
1416    #[cfg(feature = "std")]
1417    {
1418        let (mut value, span_tree) = parser::parse_one(s, &parse_config)?;
1419        apply_includes(&mut value, config)?;
1420        apply_properties(&mut value, config)?;
1421        for p in &config.policies {
1422            p.check_value(&value)?;
1423        }
1424        let spans = span_context::build_span_map(&value, &span_tree);
1425        let ctx = span_context::SpanContext {
1426            spans,
1427            source: s.into(),
1428        };
1429        let _guard = span_context::set_span_context(ctx);
1430        let de = Deserializer::with_options(
1431            &value,
1432            Some(_guard.as_ref()),
1433            config.ignore_binary_tag_for_string,
1434        );
1435        T::deserialize(de)
1436    }
1437
1438    #[cfg(not(feature = "std"))]
1439    {
1440        let value = parser::parse_one_value(s, &parse_config)?;
1441        let de = Deserializer::with_options(&value, None, config.ignore_binary_tag_for_string);
1442        T::deserialize(de)
1443    }
1444}
1445
1446/// Returns `true` when no `${KEY}` substitution table is active —
1447/// keeps the streaming fast-path eligible. Always `true` under
1448/// `no_std` (the field doesn't exist).
1449#[cfg(feature = "std")]
1450#[inline]
1451fn properties_inactive(config: &ParserConfig) -> bool {
1452    config.properties.is_none()
1453}
1454
1455#[cfg(not(feature = "std"))]
1456#[inline]
1457fn properties_inactive(_config: &ParserConfig) -> bool {
1458    true
1459}
1460
1461/// Walk the `Value` tree and substitute every `${name}` placeholder
1462/// against `config.properties`. No-op when no map is installed; in
1463/// `no_std` builds the function is also a no-op (the field doesn't
1464/// exist). Honours `strict_properties` for the missing-key
1465/// behaviour, but always propagates *syntax* errors from the
1466/// substitution walk (invalid characters, unterminated `${...}`,
1467/// malformed `:-default`) regardless of mode.
1468#[cfg(feature = "std")]
1469fn apply_properties(value: &mut Value, config: &ParserConfig) -> Result<()> {
1470    if let Some(props) = config.properties.as_ref() {
1471        let action = if config.strict_properties {
1472            crate::value::MissingAction::Error(false)
1473        } else {
1474            crate::value::MissingAction::Empty
1475        };
1476        value.interpolate_inner(
1477            &|name| match props.get(name) {
1478                Some(v) => crate::value::ResolveOutcome::Found(v.clone()),
1479                None => crate::value::ResolveOutcome::Missing,
1480            },
1481            action,
1482        )?;
1483    }
1484    Ok(())
1485}
1486
1487#[cfg(not(feature = "std"))]
1488#[inline]
1489fn apply_properties(_value: &mut Value, _config: &ParserConfig) -> Result<()> {
1490    Ok(())
1491}
1492
1493/// Returns `true` when no `!include` resolver is installed —
1494/// keeps the streaming fast-path eligible.
1495#[cfg(feature = "include")]
1496#[inline]
1497fn includes_inactive(config: &ParserConfig) -> bool {
1498    config.include_resolver.is_none()
1499}
1500
1501#[cfg(not(feature = "include"))]
1502#[inline]
1503fn includes_inactive(_config: &ParserConfig) -> bool {
1504    true
1505}
1506
1507/// Walk the `Value` tree, find every `Value::Tagged(!include, _)`
1508/// node, and replace it with the resolver's output. Cyclic
1509/// includes are detected via a per-walk visited set; depth is
1510/// bounded by `config.max_include_depth`. No-op when no resolver
1511/// is installed.
1512#[cfg(feature = "include")]
1513fn apply_includes(value: &mut Value, config: &ParserConfig) -> Result<()> {
1514    if let Some(resolver) = config.include_resolver.as_ref() {
1515        let parse_config = parser::ParseConfig::from(config);
1516        let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
1517        let mut next_id: usize = 1;
1518        resolve_includes_recursive(
1519            value,
1520            resolver,
1521            &parse_config,
1522            config.max_include_depth,
1523            0,
1524            0,
1525            &mut visited,
1526            &mut next_id,
1527        )?;
1528    }
1529    Ok(())
1530}
1531
1532#[cfg(not(feature = "include"))]
1533#[inline]
1534fn apply_includes(_value: &mut Value, _config: &ParserConfig) -> Result<()> {
1535    Ok(())
1536}
1537
1538#[cfg(feature = "include")]
1539#[allow(clippy::too_many_arguments)]
1540fn resolve_includes_recursive(
1541    value: &mut Value,
1542    resolver: &crate::include::IncludeResolver,
1543    parse_config: &parser::ParseConfig,
1544    max_depth: usize,
1545    depth: usize,
1546    from_id: usize,
1547    visited: &mut std::collections::HashSet<String>,
1548    next_id: &mut usize,
1549) -> Result<()> {
1550    if depth > max_depth {
1551        return Err(Error::RecursionLimitExceeded { depth });
1552    }
1553    match value {
1554        Value::Tagged(boxed) => {
1555            if boxed.tag().as_str() == "!include" {
1556                let spec = match boxed.value().as_str() {
1557                    Some(s) => s.to_string(),
1558                    None => {
1559                        return Err(Error::Custom(
1560                            "!include directive expects a scalar string spec".into(),
1561                        ));
1562                    }
1563                };
1564                if !visited.insert(spec.clone()) {
1565                    return Err(Error::Custom(format!(
1566                        "!include cycle detected: `{spec}` already in resolution chain"
1567                    )));
1568                }
1569                let (path, fragment) = crate::include::split_fragment(&spec);
1570                let req = crate::include::IncludeRequest {
1571                    spec: &spec,
1572                    from_id,
1573                    depth,
1574                };
1575                let source = resolver.resolve(req)?;
1576                let id = *next_id;
1577                *next_id += 1;
1578                let mut included = parser::parse_one_value(&source.bytes, parse_config)?;
1579                // Recurse into the included document's own
1580                // `!include` nodes — depth + 1.
1581                resolve_includes_recursive(
1582                    &mut included,
1583                    resolver,
1584                    parse_config,
1585                    max_depth,
1586                    depth + 1,
1587                    id,
1588                    visited,
1589                    next_id,
1590                )?;
1591                // Fragment selection: if `spec` was `foo.yaml#anchor`,
1592                // narrow to the named anchor inside the included
1593                // document. Fragments resolve against mapping keys
1594                // (the conventional YAML "anchor-as-key" pattern);
1595                // see `examples/include_directive.rs`.
1596                if let Some(frag) = fragment {
1597                    if let Some(map) = included.as_mapping() {
1598                        match map.get(frag) {
1599                            Some(v) => *value = v.clone(),
1600                            None => {
1601                                return Err(Error::Custom(format!(
1602                                    "!include fragment `#{frag}` not found in `{path}`"
1603                                )));
1604                            }
1605                        }
1606                    } else {
1607                        return Err(Error::Custom(format!(
1608                            "!include fragment `#{frag}` requires a mapping-shaped \
1609                             included document; `{path}` is not a mapping"
1610                        )));
1611                    }
1612                } else {
1613                    *value = included;
1614                }
1615                let _ = visited.remove(&spec);
1616            } else {
1617                resolve_includes_recursive(
1618                    boxed.value_mut(),
1619                    resolver,
1620                    parse_config,
1621                    max_depth,
1622                    depth,
1623                    from_id,
1624                    visited,
1625                    next_id,
1626                )?;
1627            }
1628        }
1629        Value::Sequence(seq) => {
1630            for v in seq {
1631                resolve_includes_recursive(
1632                    v,
1633                    resolver,
1634                    parse_config,
1635                    max_depth,
1636                    depth,
1637                    from_id,
1638                    visited,
1639                    next_id,
1640                )?;
1641            }
1642        }
1643        Value::Mapping(map) => {
1644            for v in map.values_mut() {
1645                resolve_includes_recursive(
1646                    v,
1647                    resolver,
1648                    parse_config,
1649                    max_depth,
1650                    depth,
1651                    from_id,
1652                    visited,
1653                    next_id,
1654                )?;
1655            }
1656        }
1657        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
1658    }
1659    Ok(())
1660}
1661
1662/// Deserialize YAML from a byte slice.
1663///
1664/// Convenience wrapper that validates `b` is UTF-8 then forwards
1665/// to [`from_str`]. Use when the caller already holds a `&[u8]`
1666/// (a buffer, a network frame, a `bytes::Bytes`) and would
1667/// otherwise have to round-trip through `String`.
1668///
1669/// # Errors
1670///
1671/// - `Error::Deserialize` — `b` is not valid UTF-8.
1672/// - All variants documented on [`from_str`].
1673///
1674/// # Examples
1675///
1676/// ```
1677/// let n: i32 = noyalib::from_slice(b"42").unwrap();
1678/// assert_eq!(n, 42);
1679/// ```
1680pub fn from_slice<T>(b: &[u8]) -> Result<T>
1681where
1682    T: for<'de> Deserialize<'de> + 'static,
1683{
1684    let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
1685    from_str(s)
1686}
1687
1688/// Deserialize YAML from a byte slice with a custom [`ParserConfig`].
1689///
1690/// # Errors
1691///
1692/// - `Error::Deserialize` — `b` is not valid UTF-8.
1693/// - All variants documented on [`from_str_with_config`].
1694///
1695/// # Examples
1696///
1697/// ```
1698/// use noyalib::{from_slice_with_config, ParserConfig};
1699/// let cfg = ParserConfig::new();
1700/// let n: i32 = from_slice_with_config(b"7", &cfg).unwrap();
1701/// assert_eq!(n, 7);
1702/// ```
1703pub fn from_slice_with_config<T>(b: &[u8], config: &ParserConfig) -> Result<T>
1704where
1705    T: for<'de> Deserialize<'de> + 'static,
1706{
1707    let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
1708    from_str_with_config(s, config)
1709}
1710
1711/// Deserialize YAML from an [`std::io::Read`] source.
1712///
1713/// Reads the entire stream into memory before parsing — YAML's
1714/// data model is not streamable past document boundaries, so this
1715/// function trades incremental I/O for a single, simple `Result`.
1716/// For very large multi-document streams prefer
1717/// [`crate::parallel::parse`] (with the `parallel` feature) which
1718/// scans document boundaries on the input thread and parses each
1719/// document in parallel.
1720///
1721/// # Errors
1722///
1723/// - `Error::Io` — the underlying reader returns an I/O error
1724///   while filling the buffer.
1725/// - All variants documented on [`from_str`].
1726///
1727/// # Examples
1728///
1729/// ```
1730/// let yaml = b"42".to_vec();
1731/// let n: i32 = noyalib::from_reader(&yaml[..]).unwrap();
1732/// assert_eq!(n, 42);
1733/// ```
1734#[cfg(feature = "std")]
1735#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
1736pub fn from_reader<R, T>(reader: R) -> Result<T>
1737where
1738    R: io::Read,
1739    T: for<'de> Deserialize<'de> + 'static,
1740{
1741    from_reader_with_config(reader, &ParserConfig::default())
1742}
1743
1744/// Deserialize YAML from an [`std::io::Read`] source with a custom
1745/// [`ParserConfig`].
1746///
1747/// # Errors
1748///
1749/// - `Error::Io` — the underlying reader returns an I/O error.
1750/// - All variants documented on [`from_str_with_config`].
1751///
1752/// # Examples
1753///
1754/// ```
1755/// use noyalib::{from_reader_with_config, ParserConfig};
1756/// let cfg = ParserConfig::new();
1757/// let bytes = b"7".to_vec();
1758/// let n: i32 = from_reader_with_config(&bytes[..], &cfg).unwrap();
1759/// assert_eq!(n, 7);
1760/// ```
1761#[cfg(feature = "std")]
1762#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
1763pub fn from_reader_with_config<R, T>(mut reader: R, config: &ParserConfig) -> Result<T>
1764where
1765    R: io::Read,
1766    T: for<'de> Deserialize<'de> + 'static,
1767{
1768    let mut s = String::new();
1769    let _ = reader.read_to_string(&mut s).map_err(Error::Io)?;
1770    from_str_with_config(&s, config)
1771}
1772
1773/// Deserialize a [`Value`] into a typed `T` via Serde's data model.
1774///
1775/// Useful for second-pass conversion when the first pass parsed
1776/// into the dynamic [`Value`] tree and a typed view is now needed
1777/// for a sub-tree.
1778///
1779/// # Errors
1780///
1781/// - `Error::Deserialize` — `value` does not match `T`'s shape.
1782/// - `Error::Custom` — surfaces upstream `serde::de::Error`
1783///   conversions that don't fit the structured variants.
1784///
1785/// # Examples
1786///
1787/// ```
1788/// use noyalib::{from_value, Value};
1789/// let v = Value::from(42_i64);
1790/// let n: i32 = from_value(&v).unwrap();
1791/// assert_eq!(n, 42);
1792/// ```
1793pub fn from_value<T>(value: &Value) -> Result<T>
1794where
1795    T: for<'de> Deserialize<'de> + 'static,
1796{
1797    // Zero-rewalk fast path: when T == Value, the answer is just
1798    // `value.clone()`. The default `T::deserialize(de)` route
1799    // walks `value` and reconstructs an identical Value via
1800    // `ValueVisitor::visit_seq`/`visit_map` — pure waste.
1801    if is_value_target::<T>() {
1802        let cloned = value.clone();
1803        let boxed: Box<dyn core::any::Any> = Box::new(cloned);
1804        let downcast: Box<T> = boxed
1805            .downcast::<T>()
1806            .expect("is_value_target proved T == Value");
1807        return Ok(*downcast);
1808    }
1809    T::deserialize(Deserializer::new(value))
1810}
1811
1812/// A YAML deserializer.
1813///
1814/// # Examples
1815///
1816/// ```
1817/// use noyalib::{Deserializer, Value};
1818/// use serde::Deserialize;
1819/// let v = Value::from(42_i64);
1820/// let de = Deserializer::new(&v);
1821/// let n: i32 = Deserialize::deserialize(de).unwrap();
1822/// assert_eq!(n, 42);
1823/// ```
1824#[derive(Debug, Clone, Copy)]
1825pub struct Deserializer<'de> {
1826    pub(crate) value: &'de Value,
1827    pub(crate) span_ctx: Option<&'de span_context::SpanContext>,
1828    /// Per-call flag mirroring
1829    /// [`ParserConfig::ignore_binary_tag_for_string`]. When `true`,
1830    /// `!!binary "ABCD"` deserializes into `String` as the literal
1831    /// `"ABCD"` (no base64 decode). Default `false` preserves YAML
1832    /// 1.2 semantics.
1833    pub(crate) ignore_binary_tag_for_string: bool,
1834    /// When `true`, [`Value::Tagged`] is surfaced through the
1835    /// magic-key [`crate::value::TagPreservingMapAccess`] so the
1836    /// outer [`Value::deserialize`] visitor can reconstruct
1837    /// `Value::Tagged(...)` losslessly. When `false` (default), a
1838    /// tagged scalar is unwrapped to its inner value — the
1839    /// transparent behaviour every typed `T::deserialize` expects.
1840    ///
1841    /// Set automatically by [`from_str_with_config`] /
1842    /// [`from_value`] when the caller's `T` is `Value` (detected
1843    /// via [`std::any::TypeId`]). Threaded through every
1844    /// `descend()` site so nested tagged values inside a
1845    /// `Mapping` / `Sequence` also survive.
1846    pub(crate) preserve_tags: bool,
1847}
1848
1849impl<'de> Deserializer<'de> {
1850    /// Create a new deserializer from a value.
1851    ///
1852    /// # Examples
1853    ///
1854    /// ```
1855    /// use noyalib::{Deserializer, Value};
1856    /// let v = Value::from(1_i64);
1857    /// let _de = Deserializer::new(&v);
1858    /// ```
1859    #[must_use]
1860    pub fn new(value: &'de Value) -> Self {
1861        Deserializer {
1862            value,
1863            span_ctx: None,
1864            ignore_binary_tag_for_string: false,
1865            preserve_tags: false,
1866        }
1867    }
1868
1869    /// Create a new deserializer from a value with an associated span context.
1870    ///
1871    /// The span context carries source-location information used to attach
1872    /// line/column details to errors and `Spanned<T>` fields. This
1873    /// constructor is primarily used internally by `from_str`; most callers
1874    /// should prefer [`Deserializer::new`].
1875    ///
1876    /// # Examples
1877    ///
1878    /// ```ignore
1879    /// // Constructed internally by from_str — external callers use Deserializer::new.
1880    /// use noyalib::Deserializer;
1881    /// # let value = unimplemented!();
1882    /// # let span_ctx = unimplemented!();
1883    /// let _de = Deserializer::with_span_context(value, span_ctx);
1884    /// ```
1885    #[must_use]
1886    pub fn with_span_context(value: &'de Value, span_ctx: &'de span_context::SpanContext) -> Self {
1887        Deserializer {
1888            value,
1889            span_ctx: Some(span_ctx),
1890            ignore_binary_tag_for_string: false,
1891            preserve_tags: false,
1892        }
1893    }
1894
1895    /// Pass-through constructor for the
1896    /// [`crate::ParserConfig::ignore_binary_tag_for_string`] flag.
1897    /// Used internally by [`from_str_with_config`] when the caller
1898    /// has opted in to the migration helper.
1899    pub(crate) fn with_options(
1900        value: &'de Value,
1901        span_ctx: Option<&'de span_context::SpanContext>,
1902        ignore_binary_tag_for_string: bool,
1903    ) -> Self {
1904        Deserializer {
1905            value,
1906            span_ctx,
1907            ignore_binary_tag_for_string,
1908            preserve_tags: false,
1909        }
1910    }
1911
1912    /// Internal constructor used by [`from_str_with_config`] /
1913    /// [`from_value`] when the caller's `T` is detected as
1914    /// [`Value`] via [`std::any::TypeId`]. Sets `preserve_tags`
1915    /// so [`Value::Tagged`] survives the data-binding return
1916    /// path. See `Deserializer::preserve_tags` for the contract.
1917    pub(crate) fn with_options_preserving_tags(
1918        value: &'de Value,
1919        span_ctx: Option<&'de span_context::SpanContext>,
1920        ignore_binary_tag_for_string: bool,
1921    ) -> Self {
1922        Deserializer {
1923            value,
1924            span_ctx,
1925            ignore_binary_tag_for_string,
1926            preserve_tags: true,
1927        }
1928    }
1929
1930    /// Construct a child deserializer for `value`, propagating the
1931    /// span context and every per-call config toggle from `self`.
1932    /// Used by every descent site (struct field, sequence element,
1933    /// tagged inner value) so the toggles survive the walk.
1934    pub(crate) fn descend(&self, value: &'de Value) -> Self {
1935        Deserializer {
1936            value,
1937            span_ctx: self.span_ctx,
1938            ignore_binary_tag_for_string: self.ignore_binary_tag_for_string,
1939            preserve_tags: self.preserve_tags,
1940        }
1941    }
1942
1943    fn wrap_err<T>(&self, res: Result<T>) -> Result<T> {
1944        match res {
1945            Err(Error::Deserialize(msg)) => {
1946                if let Some(ctx) = self.span_ctx {
1947                    let ptr: *const Value = self.value;
1948                    let addr = ptr as usize;
1949                    if let Some(span) = ctx.spans.get(&addr) {
1950                        return Err(Error::deserialize_at(msg, &ctx.source, span.0));
1951                    }
1952                }
1953                Err(Error::Deserialize(msg))
1954            }
1955            _ => res,
1956        }
1957    }
1958}
1959
1960impl<'de> de::Deserializer<'de> for Deserializer<'de> {
1961    type Error = Error;
1962
1963    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
1964    where
1965        V: Visitor<'de>,
1966    {
1967        match self.value {
1968            Value::Null => self.wrap_err(visitor.visit_none()),
1969            Value::Bool(b) => self.wrap_err(visitor.visit_bool(*b)),
1970            Value::Number(Number::Integer(n)) => self.wrap_err(visitor.visit_i64(*n)),
1971            Value::Number(Number::Float(n)) => self.wrap_err(visitor.visit_f64(*n)),
1972            Value::String(s) => self.wrap_err(visitor.visit_str(s)),
1973            Value::Sequence(_) => self.deserialize_seq(visitor),
1974            Value::Mapping(_) => self.deserialize_map(visitor),
1975            Value::Tagged(tagged) => {
1976                if self.preserve_tags {
1977                    // Tag-preserving path (`from_str::<Value>` /
1978                    // `from_value::<Value>`): surface the tag via
1979                    // a magic-key MapAccess so the outer
1980                    // `Value::deserialize` visitor reconstructs
1981                    // `Value::Tagged(...)` losslessly.
1982                    self.wrap_err(visitor.visit_map(crate::value::TagPreservingMapAccess::new(
1983                        tagged.tag().as_str(),
1984                        tagged.value(),
1985                    )))
1986                } else {
1987                    // Default path: typed targets see through the
1988                    // tag transparently — `#[derive(Deserialize)]
1989                    // struct Foo { x: i32 }` against `!Foo {x: 1}`
1990                    // yields `Foo { x: 1 }`.
1991                    let de = self.descend(tagged.value());
1992                    de.deserialize_any(visitor)
1993                }
1994            }
1995        }
1996    }
1997
1998    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
1999    where
2000        V: Visitor<'de>,
2001    {
2002        match self.value {
2003            Value::Bool(b) => self.wrap_err(visitor.visit_bool(*b)),
2004            _ => self.wrap_err(Err(Error::TypeMismatch {
2005                expected: "bool",
2006                found: type_name(self.value),
2007            })),
2008        }
2009    }
2010
2011    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value>
2012    where
2013        V: Visitor<'de>,
2014    {
2015        self.deserialize_i64(visitor)
2016    }
2017
2018    fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value>
2019    where
2020        V: Visitor<'de>,
2021    {
2022        self.deserialize_i64(visitor)
2023    }
2024
2025    fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value>
2026    where
2027        V: Visitor<'de>,
2028    {
2029        self.deserialize_i64(visitor)
2030    }
2031
2032    fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value>
2033    where
2034        V: Visitor<'de>,
2035    {
2036        match self.value {
2037            Value::Number(Number::Integer(n)) => self.wrap_err(visitor.visit_i64(*n)),
2038            Value::Number(Number::Float(n))
2039                if n.fract() == 0.0
2040                    && *n >= i64::MIN as f64
2041                    && *n <= i64::MAX as f64
2042                    && !n.is_nan() =>
2043            {
2044                self.wrap_err(visitor.visit_i64(*n as i64))
2045            }
2046            _ => self.wrap_err(Err(Error::TypeMismatch {
2047                expected: "integer",
2048                found: type_name(self.value),
2049            })),
2050        }
2051    }
2052
2053    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value>
2054    where
2055        V: Visitor<'de>,
2056    {
2057        self.deserialize_u64(visitor)
2058    }
2059
2060    fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value>
2061    where
2062        V: Visitor<'de>,
2063    {
2064        self.deserialize_u64(visitor)
2065    }
2066
2067    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value>
2068    where
2069        V: Visitor<'de>,
2070    {
2071        self.deserialize_u64(visitor)
2072    }
2073
2074    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value>
2075    where
2076        V: Visitor<'de>,
2077    {
2078        match self.value {
2079            Value::Number(Number::Integer(n)) if *n >= 0 => {
2080                self.wrap_err(visitor.visit_u64(*n as u64))
2081            }
2082            Value::Number(Number::Float(n))
2083                if n.fract() == 0.0 && *n >= 0.0 && *n <= u64::MAX as f64 && !n.is_nan() =>
2084            {
2085                self.wrap_err(visitor.visit_u64(*n as u64))
2086            }
2087            _ => self.wrap_err(Err(Error::TypeMismatch {
2088                expected: "unsigned integer",
2089                found: type_name(self.value),
2090            })),
2091        }
2092    }
2093
2094    fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value>
2095    where
2096        V: Visitor<'de>,
2097    {
2098        self.deserialize_f64(visitor)
2099    }
2100
2101    fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value>
2102    where
2103        V: Visitor<'de>,
2104    {
2105        match self.value {
2106            Value::Number(Number::Float(n)) => self.wrap_err(visitor.visit_f64(*n)),
2107            Value::Number(Number::Integer(n)) => self.wrap_err(visitor.visit_f64(*n as f64)),
2108            _ => self.wrap_err(Err(Error::TypeMismatch {
2109                expected: "float",
2110                found: type_name(self.value),
2111            })),
2112        }
2113    }
2114
2115    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value>
2116    where
2117        V: Visitor<'de>,
2118    {
2119        match self.value {
2120            Value::String(s) if s.chars().count() == 1 => {
2121                self.wrap_err(visitor.visit_char(s.chars().next().unwrap()))
2122            }
2123            _ => self.wrap_err(Err(Error::TypeMismatch {
2124                expected: "char",
2125                found: type_name(self.value),
2126            })),
2127        }
2128    }
2129
2130    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value>
2131    where
2132        V: Visitor<'de>,
2133    {
2134        match self.value {
2135            Value::String(s) => self.wrap_err(visitor.visit_str(s)),
2136            // Migration helper: when the source declared
2137            // `!!binary "ABCD"` and the caller opted in to
2138            // `ignore_binary_tag_for_string`, surface the literal
2139            // source string rather than rejecting on tag mismatch.
2140            // The base64 encoding stays as the user-facing value;
2141            // the application layer can decode (or not) as it
2142            // sees fit.
2143            Value::Tagged(boxed)
2144                if self.ignore_binary_tag_for_string && is_binary_tag(boxed.tag().as_str()) =>
2145            {
2146                match boxed.value() {
2147                    Value::String(s) => self.wrap_err(visitor.visit_str(s)),
2148                    other => self.wrap_err(Err(Error::TypeMismatch {
2149                        expected: "string-shaped !!binary content",
2150                        found: type_name(other),
2151                    })),
2152                }
2153            }
2154            _ => self.wrap_err(Err(Error::TypeMismatch {
2155                expected: "string",
2156                found: type_name(self.value),
2157            })),
2158        }
2159    }
2160
2161    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value>
2162    where
2163        V: Visitor<'de>,
2164    {
2165        self.deserialize_str(visitor)
2166    }
2167
2168    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value>
2169    where
2170        V: Visitor<'de>,
2171    {
2172        match self.value {
2173            Value::String(s) => self.wrap_err(visitor.visit_bytes(s.as_bytes())),
2174            // YAML 1.2.2 §10.4: `!!binary` carries an RFC 4648
2175            // base64-encoded payload. Decode on demand when a serde
2176            // target asks for bytes / a byte buffer (Vec<u8>,
2177            // serde_bytes::ByteBuf, &[u8] via owned visit).
2178            Value::Tagged(boxed) if is_binary_tag(boxed.tag().as_str()) => match boxed.value() {
2179                Value::String(s) => match crate::base64::decode(s) {
2180                    Ok(bytes) => self.wrap_err(visitor.visit_byte_buf(bytes)),
2181                    Err(why) => self.wrap_err(Err(Error::Deserialize(format!("!!binary: {why}")))),
2182                },
2183                other => self.wrap_err(Err(Error::TypeMismatch {
2184                    expected: "string-shaped !!binary content",
2185                    found: type_name(other),
2186                })),
2187            },
2188            _ => self.wrap_err(Err(Error::TypeMismatch {
2189                expected: "bytes",
2190                found: type_name(self.value),
2191            })),
2192        }
2193    }
2194
2195    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value>
2196    where
2197        V: Visitor<'de>,
2198    {
2199        self.deserialize_bytes(visitor)
2200    }
2201
2202    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
2203    where
2204        V: Visitor<'de>,
2205    {
2206        match self.value {
2207            Value::Null => self.wrap_err(visitor.visit_none()),
2208            _ => self.wrap_err(visitor.visit_some(self)),
2209        }
2210    }
2211
2212    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
2213    where
2214        V: Visitor<'de>,
2215    {
2216        match self.value {
2217            Value::Null => self.wrap_err(visitor.visit_unit()),
2218            _ => self.wrap_err(Err(Error::TypeMismatch {
2219                expected: "null",
2220                found: type_name(self.value),
2221            })),
2222        }
2223    }
2224
2225    fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
2226    where
2227        V: Visitor<'de>,
2228    {
2229        self.deserialize_unit(visitor)
2230    }
2231
2232    fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result<V::Value>
2233    where
2234        V: Visitor<'de>,
2235    {
2236        if name == crate::spanned::SPANNED_TYPE_NAME {
2237            return visitor.visit_map(SpannedMapAccess::new(self.value, self.span_ctx));
2238        }
2239        self.wrap_err(visitor.visit_newtype_struct(self))
2240    }
2241
2242    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
2243    where
2244        V: Visitor<'de>,
2245    {
2246        match self.value {
2247            Value::Sequence(seq) => {
2248                self.wrap_err(visitor.visit_seq(ValueSeqAccess::from_de(&self, seq)))
2249            }
2250            // Tagged values are transparent for typed `deserialize_*`
2251            // calls — `Vec<T>::deserialize` against `!List [1, 2, 3]`
2252            // (which now surfaces as `Tagged(Sequence(...))` per the
2253            // tag-preserving loader) sees through the wrapper.
2254            Value::Tagged(tagged) => self.descend(tagged.value()).deserialize_seq(visitor),
2255            _ => self.wrap_err(Err(Error::TypeMismatch {
2256                expected: "sequence",
2257                found: type_name(self.value),
2258            })),
2259        }
2260    }
2261
2262    fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
2263    where
2264        V: Visitor<'de>,
2265    {
2266        self.deserialize_seq(visitor)
2267    }
2268
2269    fn deserialize_tuple_struct<V>(
2270        self,
2271        _name: &'static str,
2272        _len: usize,
2273        visitor: V,
2274    ) -> Result<V::Value>
2275    where
2276        V: Visitor<'de>,
2277    {
2278        self.deserialize_seq(visitor)
2279    }
2280
2281    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
2282    where
2283        V: Visitor<'de>,
2284    {
2285        match self.value {
2286            Value::Mapping(map) => {
2287                self.wrap_err(visitor.visit_map(ValueMapAccess::from_de(&self, map)))
2288            }
2289            // Tagged values are transparent for typed
2290            // `deserialize_*` calls — `HashMap::deserialize`
2291            // against `!!set { Mark, Sammy }` (which now surfaces
2292            // as `Tagged(Mapping(...))` per the tag-preserving
2293            // loader) sees through the wrapper.
2294            Value::Tagged(tagged) => self.descend(tagged.value()).deserialize_map(visitor),
2295            _ => self.wrap_err(Err(Error::TypeMismatch {
2296                expected: "mapping",
2297                found: type_name(self.value),
2298            })),
2299        }
2300    }
2301
2302    fn deserialize_struct<V>(
2303        self,
2304        name: &'static str,
2305        _fields: &'static [&'static str],
2306        visitor: V,
2307    ) -> Result<V::Value>
2308    where
2309        V: Visitor<'de>,
2310    {
2311        if name == crate::spanned::SPANNED_TYPE_NAME {
2312            return visitor.visit_map(SpannedMapAccess::new(self.value, self.span_ctx));
2313        }
2314        self.deserialize_map(visitor)
2315    }
2316
2317    fn deserialize_enum<V>(
2318        self,
2319        _name: &'static str,
2320        _variants: &'static [&'static str],
2321        visitor: V,
2322    ) -> Result<V::Value>
2323    where
2324        V: Visitor<'de>,
2325    {
2326        match self.value {
2327            Value::String(variant) => {
2328                let de: de::value::StrDeserializer<'de, Error> =
2329                    variant.as_str().into_deserializer();
2330                self.wrap_err(visitor.visit_enum(de))
2331            }
2332            Value::Mapping(map) if map.len() == 1 => {
2333                let (variant, value) = map.iter().next().unwrap();
2334                self.wrap_err(visitor.visit_enum(EnumAccess {
2335                    variant,
2336                    value,
2337                    span_ctx: self.span_ctx,
2338                }))
2339            }
2340            _ => self.wrap_err(Err(Error::TypeMismatch {
2341                expected: "string or single-key mapping",
2342                found: type_name(self.value),
2343            })),
2344        }
2345    }
2346
2347    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
2348    where
2349        V: Visitor<'de>,
2350    {
2351        match self.value {
2352            Value::String(s) => self.wrap_err(visitor.visit_str(s)),
2353            _ => self.deserialize_any(visitor),
2354        }
2355    }
2356
2357    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value>
2358    where
2359        V: Visitor<'de>,
2360    {
2361        self.wrap_err(visitor.visit_unit())
2362    }
2363}
2364
2365pub(crate) struct ValueSeqAccess<'de> {
2366    iter: core::slice::Iter<'de, Value>,
2367    span_ctx: Option<&'de span_context::SpanContext>,
2368    ignore_binary_tag_for_string: bool,
2369    /// Mirror of the parent [`Deserializer::preserve_tags`] so
2370    /// nested `Value::Tagged(...)` nodes inside a sequence
2371    /// survive the data-binding return path.
2372    preserve_tags: bool,
2373}
2374
2375impl<'de> ValueSeqAccess<'de> {
2376    pub(crate) fn from_de(de: &Deserializer<'de>, seq: &'de [Value]) -> Self {
2377        ValueSeqAccess {
2378            iter: seq.iter(),
2379            span_ctx: de.span_ctx,
2380            ignore_binary_tag_for_string: de.ignore_binary_tag_for_string,
2381            preserve_tags: de.preserve_tags,
2382        }
2383    }
2384}
2385
2386impl<'de> SeqAccess<'de> for ValueSeqAccess<'de> {
2387    type Error = Error;
2388
2389    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
2390    where
2391        T: DeserializeSeed<'de>,
2392    {
2393        match self.iter.next() {
2394            Some(value) => {
2395                let de = Deserializer {
2396                    value,
2397                    span_ctx: self.span_ctx,
2398                    ignore_binary_tag_for_string: self.ignore_binary_tag_for_string,
2399                    preserve_tags: self.preserve_tags,
2400                };
2401                seed.deserialize(de).map(Some)
2402            }
2403            None => Ok(None),
2404        }
2405    }
2406}
2407
2408pub(crate) struct ValueMapAccess<'de> {
2409    iter: indexmap::map::Iter<'de, String, Value>,
2410    value: Option<&'de Value>,
2411    span_ctx: Option<&'de span_context::SpanContext>,
2412    ignore_binary_tag_for_string: bool,
2413    /// Mirror of the parent [`Deserializer::preserve_tags`] so
2414    /// nested `Value::Tagged(...)` nodes inside a mapping survive
2415    /// the data-binding return path. See `de::Deserializer` docs.
2416    preserve_tags: bool,
2417}
2418
2419impl<'de> ValueMapAccess<'de> {
2420    pub(crate) fn from_de(de: &Deserializer<'de>, map: &'de crate::value::Mapping) -> Self {
2421        ValueMapAccess {
2422            iter: map.iter(),
2423            value: None,
2424            span_ctx: de.span_ctx,
2425            ignore_binary_tag_for_string: de.ignore_binary_tag_for_string,
2426            preserve_tags: de.preserve_tags,
2427        }
2428    }
2429
2430    /// Build the child [`Deserializer`] used to read each map
2431    /// value — propagates every per-call toggle including
2432    /// `preserve_tags`.
2433    fn child_de(&self, value: &'de Value) -> Deserializer<'de> {
2434        Deserializer {
2435            value,
2436            span_ctx: self.span_ctx,
2437            ignore_binary_tag_for_string: self.ignore_binary_tag_for_string,
2438            preserve_tags: self.preserve_tags,
2439        }
2440    }
2441}
2442
2443impl<'de> MapAccess<'de> for ValueMapAccess<'de> {
2444    type Error = Error;
2445
2446    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
2447    where
2448        K: DeserializeSeed<'de>,
2449    {
2450        match self.iter.next() {
2451            Some((key, value)) => {
2452                self.value = Some(value);
2453                let de = self.child_de(value);
2454                let key_de: de::value::StrDeserializer<'de, Error> =
2455                    key.as_str().into_deserializer();
2456                de.wrap_err(seed.deserialize(key_de).map(Some))
2457            }
2458            None => Ok(None),
2459        }
2460    }
2461
2462    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
2463    where
2464        V: DeserializeSeed<'de>,
2465    {
2466        match self.value.take() {
2467            Some(value) => {
2468                let de = self.child_de(value);
2469                let res = seed.deserialize(de);
2470                de.wrap_err(res)
2471            }
2472            None => Err(de::Error::custom("value is missing")),
2473        }
2474    }
2475}
2476
2477struct EnumAccess<'de> {
2478    variant: &'de str,
2479    value: &'de Value,
2480    span_ctx: Option<&'de span_context::SpanContext>,
2481}
2482
2483impl<'de> de::EnumAccess<'de> for EnumAccess<'de> {
2484    type Error = Error;
2485    type Variant = VariantAccess<'de>;
2486
2487    fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
2488    where
2489        V: DeserializeSeed<'de>,
2490    {
2491        let de: de::value::StrDeserializer<'de, Error> = self.variant.into_deserializer();
2492        let variant = seed.deserialize(de)?;
2493        let visitor = VariantAccess {
2494            value: self.value,
2495            span_ctx: self.span_ctx,
2496        };
2497        Ok((variant, visitor))
2498    }
2499}
2500
2501struct VariantAccess<'de> {
2502    value: &'de Value,
2503    span_ctx: Option<&'de span_context::SpanContext>,
2504}
2505
2506impl<'de> de::VariantAccess<'de> for VariantAccess<'de> {
2507    type Error = Error;
2508
2509    fn unit_variant(self) -> Result<()> {
2510        let de = if let Some(ctx) = self.span_ctx {
2511            Deserializer::with_span_context(self.value, ctx)
2512        } else {
2513            Deserializer::new(self.value)
2514        };
2515        Deserialize::deserialize(de)
2516    }
2517
2518    fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
2519    where
2520        T: DeserializeSeed<'de>,
2521    {
2522        let de = if let Some(ctx) = self.span_ctx {
2523            Deserializer::with_span_context(self.value, ctx)
2524        } else {
2525            Deserializer::new(self.value)
2526        };
2527        seed.deserialize(de)
2528    }
2529
2530    fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
2531    where
2532        V: Visitor<'de>,
2533    {
2534        let de = if let Some(ctx) = self.span_ctx {
2535            Deserializer::with_span_context(self.value, ctx)
2536        } else {
2537            Deserializer::new(self.value)
2538        };
2539        de::Deserializer::deserialize_seq(de, visitor)
2540    }
2541
2542    fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
2543    where
2544        V: Visitor<'de>,
2545    {
2546        let de = if let Some(ctx) = self.span_ctx {
2547            Deserializer::with_span_context(self.value, ctx)
2548        } else {
2549            Deserializer::new(self.value)
2550        };
2551        de::Deserializer::deserialize_map(de, visitor)
2552    }
2553}
2554
2555pub(crate) struct SpannedMapAccess<'de> {
2556    value: &'de Value,
2557    span_ctx: Option<&'de span_context::SpanContext>,
2558    fields: core::slice::Iter<'static, &'static str>,
2559}
2560
2561impl<'de> SpannedMapAccess<'de> {
2562    pub(crate) fn new(value: &'de Value, span_ctx: Option<&'de span_context::SpanContext>) -> Self {
2563        SpannedMapAccess {
2564            value,
2565            span_ctx,
2566            fields: crate::spanned::SPANNED_FIELDS.iter(),
2567        }
2568    }
2569}
2570
2571impl<'de> MapAccess<'de> for SpannedMapAccess<'de> {
2572    type Error = Error;
2573
2574    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
2575    where
2576        K: DeserializeSeed<'de>,
2577    {
2578        match self.fields.next() {
2579            Some(field) => {
2580                use serde::de::value::BorrowedStrDeserializer;
2581                let de: BorrowedStrDeserializer<'_, Error> = BorrowedStrDeserializer::new(field);
2582                seed.deserialize(de).map(Some)
2583            }
2584            None => Ok(None),
2585        }
2586    }
2587
2588    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
2589    where
2590        V: DeserializeSeed<'de>,
2591    {
2592        use crate::spanned::*;
2593        let last_field = SPANNED_FIELDS[SPANNED_FIELDS.len() - 1 - (self.fields.len())];
2594
2595        if last_field == SPANNED_FIELD_VALUE {
2596            let de = if let Some(ctx) = self.span_ctx {
2597                Deserializer::with_span_context(self.value, ctx)
2598            } else {
2599                Deserializer::new(self.value)
2600            };
2601            return de.wrap_err(seed.deserialize(de));
2602        }
2603
2604        let ptr: *const Value = self.value;
2605        let addr = ptr as usize;
2606        let span = self.span_ctx.and_then(|ctx| ctx.spans.get(&addr));
2607        let loc = if let Some(s) = span {
2608            crate::error::Location::from_index(&self.span_ctx.unwrap().source, s.0)
2609        } else {
2610            crate::error::Location::default()
2611        };
2612        let end_loc = if let Some(s) = span {
2613            crate::error::Location::from_index(&self.span_ctx.unwrap().source, s.1)
2614        } else {
2615            crate::error::Location::default()
2616        };
2617
2618        let val = match last_field {
2619            SPANNED_FIELD_START_LINE => loc.line(),
2620            SPANNED_FIELD_START_COLUMN => loc.column(),
2621            SPANNED_FIELD_START_INDEX => loc.index(),
2622            SPANNED_FIELD_END_LINE => end_loc.line(),
2623            SPANNED_FIELD_END_COLUMN => end_loc.column(),
2624            SPANNED_FIELD_END_INDEX => end_loc.index(),
2625            _ => crate::error::invariant_violated(
2626                "spanned-field index outside the SPANNED_FIELDS array",
2627            ),
2628        };
2629
2630        seed.deserialize(val.into_deserializer())
2631    }
2632}
2633
2634/// True if `tag` names the YAML 1.2 binary tag, in any of the forms
2635/// the scanner / loader may produce: shorthand `!!binary`, suffix
2636/// `binary` (post-handle-stripping), or the canonical full URI
2637/// `tag:yaml.org,2002:binary`. Stripping the leading `!` on the
2638/// shorthand keeps `Tag::new("!!binary") == Tag::new("binary")` —
2639/// which noyalib's `Tag` already considers equal — both matching.
2640pub(crate) fn is_binary_tag(tag: &str) -> bool {
2641    matches!(
2642        tag,
2643        "!!binary" | "binary" | "tag:yaml.org,2002:binary" | "!<tag:yaml.org,2002:binary>"
2644    )
2645}
2646
2647fn type_name(value: &Value) -> String {
2648    match value {
2649        Value::Null => "null".to_owned(),
2650        Value::Bool(_) => "bool".to_owned(),
2651        Value::Number(Number::Integer(_)) => "integer".to_owned(),
2652        Value::Number(Number::Float(_)) => "float".to_owned(),
2653        Value::String(_) => "string".to_owned(),
2654        Value::Sequence(_) => "sequence".to_owned(),
2655        Value::Mapping(_) => "mapping".to_owned(),
2656        Value::Tagged(tagged) => format!("tagged value (!{})", tagged.tag().as_str()),
2657    }
2658}