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