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(®));
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}