Skip to main content

noyalib/de/
config.rs

1//! Parser configuration types.
2
3// SPDX-License-Identifier: MIT OR Apache-2.0
4// Copyright (c) 2026 Noyalib. All rights reserved.
5
6use crate::prelude::*;
7
8/// Which version of the YAML specification the resolver follows.
9///
10/// YAML 1.2 (the default) and 1.1 differ in their plain-scalar
11/// resolution table:
12///
13/// | Form | 1.2 (core schema) | 1.1 |
14/// |---|---|---|
15/// | `yes` / `no` / `on` / `off` | string | bool |
16/// | `0644` | int 644 (decimal) | int 420 (octal) |
17/// | `60:00` | string | int 3 600 (sexagesimal) |
18/// | `.nan` / `.inf` | float | float (same) |
19/// | `true` / `false` | bool | bool (same) |
20///
21/// Selecting a version is a preset over the three `legacy_*` flags;
22/// see [`ParserConfig::version`] for the full mapping.
23///
24/// # Examples
25///
26/// ```
27/// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
28///
29/// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
30/// let v: Value = from_str_with_config("yes", &cfg).unwrap();
31/// assert_eq!(v, Value::Bool(true));
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34#[non_exhaustive]
35pub enum YamlVersion {
36    /// YAML 1.2 (2009) core schema. Default. Strict `true` / `false`
37    /// booleans; no bare octal; no sexagesimal.
38    #[default]
39    V1_2,
40    /// YAML 1.1 (2005). Broad resolver: `yes` / `no` / `on` / `off`
41    /// are booleans; `0644` is octal; `60:00` is sexagesimal.
42    V1_1,
43}
44
45/// Deserialization configuration.
46///
47/// All fields are public, but the struct is annotated
48/// [`#[non_exhaustive]`][nex] so that adding a new budget or
49/// policy in a future minor release is **not** a breaking change.
50/// Construct with [`ParserConfig::new`] / [`ParserConfig::strict`]
51/// / [`ParserConfig::default`] (preferred) or with the
52/// `..ParserConfig::default()` struct-update form; do not
53/// construct from an exhaustive struct-literal outside this
54/// crate.
55///
56/// [nex]: https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute
57///
58/// # Examples
59///
60/// ```
61/// use noyalib::ParserConfig;
62/// let cfg = ParserConfig::new().max_depth(64);
63/// assert_eq!(cfg.max_depth, 64);
64/// ```
65#[derive(Debug, Clone)]
66#[non_exhaustive]
67pub struct ParserConfig {
68    /// Which YAML specification version to honour during plain-scalar
69    /// resolution.
70    ///
71    /// YAML 1.2 (default) follows the **core schema** — strict
72    /// `true`/`false` booleans, no bare `0`-prefix octal, no
73    /// sexagesimal `60:00` integers. YAML 1.1 broadens the resolver
74    /// to accept all of those legacy forms.
75    ///
76    /// Setting this to [`YamlVersion::V1_1`] is equivalent to flipping
77    /// every `legacy_*` flag (`legacy_booleans`, `legacy_octal_numbers`,
78    /// `legacy_sexagesimal`) on at once. The `legacy_*` flags remain
79    /// available for fine-grained overrides — version selection sets a
80    /// preset, individual flags refine it.
81    pub yaml_version: YamlVersion,
82    /// Maximum recursion depth allowed during parsing (default: 128).
83    pub max_depth: usize,
84    /// Maximum length of a single YAML document in bytes (default: 64 MB).
85    pub max_document_length: usize,
86    /// Maximum number of times a single anchor can be expanded (default: 1024).
87    pub max_alias_expansions: usize,
88    /// Maximum number of keys allowed in a single mapping (default: 64k).
89    pub max_mapping_keys: usize,
90    /// Maximum number of elements allowed in a single sequence (default: 64k).
91    pub max_sequence_length: usize,
92    /// Maximum total parser events emitted across the input
93    /// (default: 1 000 000). Caps event-stream amplification
94    /// independent of recursion depth or alias count. Trips
95    /// [`crate::Error::Budget`] with
96    /// [`crate::BudgetBreach::MaxEvents`].
97    pub max_events: usize,
98    /// Maximum total `Value` nodes authored into the AST across the
99    /// input (default: 250 000). Each scalar, sequence, and mapping —
100    /// empty collections included — counts as one node, so this bounds
101    /// node-dense payloads (long runs of `[]`/`{}`) that stay under the
102    /// scalar-byte and event caps. Trips [`crate::Error::Budget`] with
103    /// [`crate::BudgetBreach::MaxNodes`]. Enforced on the AST-loader
104    /// path; raise it for deliberately large documents.
105    pub max_nodes: usize,
106    /// Maximum cumulative scalar-byte count across the document
107    /// (default: 64 MB). Distinct from
108    /// [`Self::max_document_length`] (input size) — this caps
109    /// scalar payload after alias expansion. Trips
110    /// [`crate::BudgetBreach::MaxTotalScalarBytes`].
111    pub max_total_scalar_bytes: usize,
112    /// Maximum number of documents in a multi-document stream
113    /// (default: 1 000). Trips
114    /// [`crate::BudgetBreach::MaxDocuments`].
115    pub max_documents: usize,
116    /// Maximum number of merge-key (`<<`) entries across the
117    /// document (default: 10 000). Trips
118    /// [`crate::BudgetBreach::MaxMergeKeys`].
119    pub max_merge_keys: usize,
120    /// Optional alias-to-anchor ratio heuristic for detecting
121    /// billion-laughs amplification patterns
122    /// (default: `Some(10.0)`). When more than `ratio × anchors`
123    /// aliases have been resolved, the parser trips
124    /// [`crate::BudgetBreach::AliasAnchorRatio`]. Set to `None`
125    /// to disable.
126    pub alias_anchor_ratio: Option<f64>,
127    /// How to handle duplicate keys in a mapping (default: Last, per YAML 1.2).
128    pub duplicate_key_policy: DuplicateKeyPolicy,
129    /// If true, only `true` and `false` (lowercase) are accepted as booleans.
130    pub strict_booleans: bool,
131    /// If true, accepts YAML 1.1 booleans like `yes`, `no`, `on`, `off`.
132    pub legacy_booleans: bool,
133    /// Optional registry of custom tags to strip on the streaming path.
134    ///
135    /// See [`TagRegistry`](crate::TagRegistry) for the full rationale.
136    /// `None` (default) preserves the legacy behaviour of routing every
137    /// custom-tagged value through the AST fallback.
138    pub tag_registry: Option<Arc<crate::TagRegistry>>,
139    /// How the YAML merge key (`<<`) should be handled.
140    ///
141    /// See [`MergeKeyPolicy`] for the available policies. The
142    /// default is [`MergeKeyPolicy::Auto`] — the YAML 1.2 spec
143    /// behaviour where `<<:` triggers automatic mapping merge.
144    pub merge_key_policy: MergeKeyPolicy,
145    /// When `true`, plain scalars are *never* resolved to
146    /// `null` / `bool` / `int` / `float` — every plain scalar
147    /// becomes a string. Useful for schema-strict pipelines that
148    /// require the user to quote intent explicitly. Default
149    /// `false`.
150    pub no_schema: bool,
151    /// When `true`, accept YAML 1.1-style bare `0`-prefix octal
152    /// literals (e.g. `0644` parsed as 420) in addition to the
153    /// YAML 1.2 `0o644` form. Default `false` to honour the YAML
154    /// 1.2 schema.
155    pub legacy_octal_numbers: bool,
156    /// When `true`, deserializing `!!binary "ABCD"` into a
157    /// [`String`] target yields the literal base64 source string
158    /// (`"ABCD"`) rather than rejecting on tag mismatch. The
159    /// canonical bytes path (`Vec<u8>`,
160    /// `serde_bytes::ByteBuf`) still decodes the base64 payload
161    /// either way. Useful for migrations from Python pyyaml-style
162    /// applications that treat the tag as advisory. Default
163    /// `false`.
164    pub ignore_binary_tag_for_string: bool,
165    /// When `true`, accept YAML 1.1-style **sexagesimal** numbers
166    /// (`60:00`, `1:30:00`) as integers. The colon-separated
167    /// digits are interpreted in base 60: each component is
168    /// multiplied by an increasing power of 60, summed left to
169    /// right. `60:00` → 3 600; `1:30:00` → 5 400. Negative values
170    /// (`-1:30:00`) and partial signs are honoured.
171    ///
172    /// Off by default to honour the YAML 1.2 schema. Useful for
173    /// migrations from YAML 1.1 / Ruby / pyyaml configs that use
174    /// the legacy time-of-day notation.
175    pub legacy_sexagesimal: bool,
176    /// When `true`, and the `lossless-u64` Cargo feature is enabled,
177    /// YAML integer scalars in `(i64::MAX, u64::MAX]` resolve as
178    /// unsigned integers instead of falling through to `f64`.
179    ///
180    /// Default `false` to preserve the historical public
181    /// `Integer(i64)` / `Float(f64)` model and serde-yaml compatibility.
182    #[cfg(feature = "lossless-u64")]
183    #[cfg_attr(docsrs, doc(cfg(feature = "lossless-u64")))]
184    pub lossless_u64_integers: bool,
185    /// Indentation-validation mode. See [`RequireIndent`].
186    /// Default: [`RequireIndent::Unchecked`] — accept any
187    /// well-formed YAML indent.
188    pub require_indent: RequireIndent,
189    /// Pluggable "Safe YAML" policies, run during parsing.
190    ///
191    /// Each [`Policy`](crate::policy::Policy) inspects parser
192    /// events and the post-parse [`Value`](crate::Value) tree; any policy
193    /// returning `Err(...)` aborts the parse with that diagnostic.
194    /// Empty by default.
195    ///
196    /// Use [`ParserConfig::with_policy`] to register a policy.
197    /// When at least one policy is present the streaming fast-path
198    /// is bypassed automatically so the policy contract holds for
199    /// every code path.
200    pub policies: Vec<Arc<dyn crate::policy::Policy>>,
201    /// `${KEY}` / `${KEY:-default}` substitution table consulted
202    /// after parsing every document.
203    ///
204    /// Each scalar in the resulting [`Value`](crate::Value) tree is walked and
205    /// any `${name}` placeholder is replaced with the property of
206    /// that name. Supported syntax:
207    ///
208    /// - `${name}` — substitute, error or pass through depending
209    ///   on [`Self::strict_properties`]
210    /// - `${name:-default}` — substitute, falling back to
211    ///   `default` when `name` is missing (always silent, never
212    ///   surfaces in errors)
213    /// - `${{` — literal `${` (escape for the open delimiter)
214    /// - `$$` — literal `$`
215    /// - `}}` — literal `}`
216    ///
217    /// `None` (default) disables the substitution pass entirely;
218    /// the parser is unchanged. Setting a non-empty map forces the
219    /// AST fallback so the post-parse walk runs uniformly across
220    /// every typed target.
221    #[cfg(feature = "std")]
222    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
223    pub properties: Option<Arc<std::collections::HashMap<String, String>>>,
224    /// When `true`, an unknown `${name}` placeholder (no entry in
225    /// [`Self::properties`] and no `:-default` fallback) aborts
226    /// the parse with [`Error::Custom`](crate::Error::Custom).
227    /// When `false` (default), unknown placeholders are replaced
228    /// with the empty string — the lossy semantics matching
229    /// [`Value::interpolate_properties_lossy`](crate::Value::interpolate_properties_lossy).
230    #[cfg(feature = "std")]
231    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
232    pub strict_properties: bool,
233    /// `!include` directive resolver. When set, the post-parse
234    /// walk substitutes every `Value::Tagged(!include, spec)`
235    /// node with the result of `resolver(IncludeRequest)`. See
236    /// [`crate::include::IncludeResolver`] for the closure
237    /// signature and [`crate::include::SafeFileResolver`] for
238    /// the bundled filesystem implementation.
239    ///
240    /// `None` (default) disables include expansion; tagged
241    /// `!include` nodes flow through unchanged.
242    #[cfg(feature = "include")]
243    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
244    pub include_resolver: Option<crate::include::IncludeResolver>,
245    /// Maximum `!include` recursion depth. Default 24. Each
246    /// nested `!include` increments the depth counter; once the
247    /// limit is reached, the parser aborts with
248    /// `Error::RecursionLimitExceeded`. Pairs with a per-walk
249    /// visited-set to catch cycles independent of depth.
250    #[cfg(feature = "include")]
251    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
252    pub max_include_depth: usize,
253}
254
255impl Default for ParserConfig {
256    fn default() -> Self {
257        Self {
258            yaml_version: YamlVersion::V1_2,
259            max_depth: 128,
260            max_document_length: 1024 * 1024 * 64, // 64 MB
261            max_alias_expansions: 1024,
262            max_mapping_keys: 1024 * 64,
263            max_sequence_length: 1024 * 64,
264            max_events: 1_000_000,
265            max_nodes: 250_000,
266            max_total_scalar_bytes: 1024 * 1024 * 64, // 64 MB
267            max_documents: 1_000,
268            max_merge_keys: 10_000,
269            alias_anchor_ratio: Some(10.0),
270            duplicate_key_policy: DuplicateKeyPolicy::default(),
271            strict_booleans: false,
272            legacy_booleans: false,
273            tag_registry: None,
274            merge_key_policy: MergeKeyPolicy::default(),
275            no_schema: false,
276            legacy_octal_numbers: false,
277            ignore_binary_tag_for_string: false,
278            legacy_sexagesimal: false,
279            #[cfg(feature = "lossless-u64")]
280            lossless_u64_integers: false,
281            require_indent: RequireIndent::Unchecked,
282            policies: Vec::new(),
283            #[cfg(feature = "std")]
284            properties: None,
285            #[cfg(feature = "std")]
286            strict_properties: false,
287            #[cfg(feature = "include")]
288            include_resolver: None,
289            #[cfg(feature = "include")]
290            max_include_depth: 24,
291        }
292    }
293}
294
295impl ParserConfig {
296    /// Create a new configuration with default values.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use noyalib::ParserConfig;
302    /// let cfg = ParserConfig::new();
303    /// assert_eq!(cfg.max_depth, 128);
304    /// ```
305    #[must_use]
306    pub fn new() -> Self {
307        Self::default()
308    }
309
310    /// Create a strict configuration (YAML 1.2 strict) with tighter
311    /// security limits suitable for untrusted input.
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use noyalib::ParserConfig;
317    /// let cfg = ParserConfig::strict();
318    /// assert_eq!(cfg.max_depth, 64);
319    /// ```
320    #[must_use]
321    pub fn strict() -> Self {
322        Self {
323            yaml_version: YamlVersion::V1_2,
324            max_depth: 64,
325            max_document_length: 1024 * 1024, // 1 MB
326            max_alias_expansions: 100,
327            max_mapping_keys: 1024,
328            max_sequence_length: 1024,
329            max_events: 100_000,
330            max_nodes: 25_000,
331            max_total_scalar_bytes: 1024 * 1024, // 1 MB
332            max_documents: 100,
333            max_merge_keys: 1_000,
334            alias_anchor_ratio: Some(5.0),
335            strict_booleans: true,
336            legacy_booleans: false,
337            duplicate_key_policy: DuplicateKeyPolicy::Error,
338            tag_registry: None,
339            merge_key_policy: MergeKeyPolicy::default(),
340            no_schema: false,
341            legacy_octal_numbers: false,
342            ignore_binary_tag_for_string: false,
343            legacy_sexagesimal: false,
344            #[cfg(feature = "lossless-u64")]
345            lossless_u64_integers: 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`](crate::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    /// Enable or disable lossless unsigned integer resolution.
900    ///
901    /// With the `lossless-u64` feature enabled, setting this to
902    /// `true` lets YAML integer scalars in `(i64::MAX, u64::MAX]`
903    /// resolve as `Number::Unsigned` instead of falling through to
904    /// `Number::Float`.
905    #[cfg(feature = "lossless-u64")]
906    #[cfg_attr(docsrs, doc(cfg(feature = "lossless-u64")))]
907    #[must_use]
908    pub fn lossless_u64_integers(mut self, on: bool) -> Self {
909        self.lossless_u64_integers = on;
910        self
911    }
912}
913
914/// Policy for handling the YAML merge key (`<<`) during parsing.
915///
916/// YAML 1.2 §10.2 defines `<<` as a "merge key" that, when used as
917/// a mapping key, splices the value's mapping (or sequence of
918/// mappings) into the enclosing mapping. The variants below let
919/// callers opt out of that behaviour.
920///
921/// # Examples
922///
923/// Indentation-validation mode for the YAML scanner.
924///
925/// Issue #6 surface — every block-context indent transition is
926/// classified per the chosen mode. The default
927/// ([`RequireIndent::Unchecked`]) accepts any well-formed YAML
928/// indent (the YAML 1.2 spec mandate), which is what every
929/// other Rust YAML parser does.
930///
931/// Stricter modes are useful in pipelines that require uniform
932/// indentation house style (linters, formatters, reviewer
933/// gates) — a config file with mixed `2`-space and `4`-space
934/// indent passes by-spec but fails consistency review.
935///
936/// # Variants
937///
938/// - [`RequireIndent::Unchecked`] (default) — by-spec mode.
939/// - [`RequireIndent::Even`] — every indent delta must be even.
940/// - `RequireIndent::Divisible(N)` — every indent delta must
941///   be divisible by `N`.
942/// - `RequireIndent::Uniform(Some(N))` — every indent delta
943///   must equal `N`. `None` means "auto-detect from the first
944///   delta and require the rest to match it".
945///
946/// # Examples
947///
948/// ```
949/// use noyalib::{ParserConfig, RequireIndent};
950/// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
951/// assert_eq!(cfg.require_indent, RequireIndent::Even);
952/// ```
953#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
954#[non_exhaustive]
955pub enum RequireIndent {
956    /// Accept any indent transition the YAML 1.2 spec allows.
957    /// Default.
958    #[default]
959    Unchecked,
960    /// Indent delta must be even (`2`, `4`, `6`, …). The most
961    /// common house-style.
962    Even,
963    /// Indent delta must be divisible by `N`.
964    Divisible(usize),
965    /// `Some(N)`: every indent delta must equal `N`.
966    /// `None`: the first delta sets the standard for the
967    /// document; subsequent deltas must match it.
968    Uniform(Option<usize>),
969}
970
971/// ```
972/// use noyalib::MergeKeyPolicy;
973/// assert_eq!(MergeKeyPolicy::default(), MergeKeyPolicy::Auto);
974/// ```
975#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
976#[non_exhaustive]
977pub enum MergeKeyPolicy {
978    /// Apply the YAML 1.2 merge-key semantics — `<<:` keys trigger
979    /// automatic merge of the value into the enclosing mapping.
980    /// Default.
981    #[default]
982    Auto,
983    /// Treat `<<` as an ordinary string key. The mapping retains a
984    /// literal `<<` entry whose value is whatever the YAML
985    /// document supplied. Useful when round-tripping configuration
986    /// files that happen to contain a `<<` key for non-merge
987    /// reasons.
988    AsOrdinary,
989    /// Reject any document that contains a `<<` key with
990    /// [`crate::Error::Custom`]. Useful for schema-strict pipelines
991    /// where merge keys are forbidden.
992    Error,
993}
994
995/// Policy for handling duplicate keys in a YAML mapping.
996///
997/// # Examples
998///
999/// ```
1000/// use noyalib::DuplicateKeyPolicy;
1001/// assert_eq!(DuplicateKeyPolicy::default(), DuplicateKeyPolicy::Last);
1002/// ```
1003#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1004#[non_exhaustive]
1005pub enum DuplicateKeyPolicy {
1006    /// Use the first occurrence of the key; ignore subsequent ones.
1007    First,
1008    /// Use the last occurrence of the key (YAML 1.2 default).
1009    #[default]
1010    Last,
1011    /// Return an error if a duplicate key is encountered.
1012    Error,
1013}