noyalib 0.0.11

A pure Rust YAML library with zero unsafe code and full serde integration
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
//! Parser configuration types.

// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.

use crate::prelude::*;

/// Which version of the YAML specification the resolver follows.
///
/// YAML 1.2 (the default) and 1.1 differ in their plain-scalar
/// resolution table:
///
/// | Form | 1.2 (core schema) | 1.1 |
/// |---|---|---|
/// | `yes` / `no` / `on` / `off` | string | bool |
/// | `0644` | int 644 (decimal) | int 420 (octal) |
/// | `60:00` | string | int 3 600 (sexagesimal) |
/// | `.nan` / `.inf` | float | float (same) |
/// | `true` / `false` | bool | bool (same) |
///
/// Selecting a version is a preset over the three `legacy_*` flags;
/// see [`ParserConfig::version`] for the full mapping.
///
/// # Examples
///
/// ```
/// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
///
/// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
/// let v: Value = from_str_with_config("yes", &cfg).unwrap();
/// assert_eq!(v, Value::Bool(true));
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum YamlVersion {
    /// YAML 1.2 (2009) core schema. Default. Strict `true` / `false`
    /// booleans; no bare octal; no sexagesimal.
    #[default]
    V1_2,
    /// YAML 1.1 (2005). Broad resolver: `yes` / `no` / `on` / `off`
    /// are booleans; `0644` is octal; `60:00` is sexagesimal.
    V1_1,
}

/// Deserialization configuration.
///
/// All fields are public, but the struct is annotated
/// [`#[non_exhaustive]`][nex] so that adding a new budget or
/// policy in a future minor release is **not** a breaking change.
/// Construct with [`ParserConfig::new`] / [`ParserConfig::strict`]
/// / [`ParserConfig::default`] (preferred) or with the
/// `..ParserConfig::default()` struct-update form; do not
/// construct from an exhaustive struct-literal outside this
/// crate.
///
/// [nex]: https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute
///
/// # Examples
///
/// ```
/// use noyalib::ParserConfig;
/// let cfg = ParserConfig::new().max_depth(64);
/// assert_eq!(cfg.max_depth, 64);
/// ```
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ParserConfig {
    /// Which YAML specification version to honour during plain-scalar
    /// resolution.
    ///
    /// YAML 1.2 (default) follows the **core schema** — strict
    /// `true`/`false` booleans, no bare `0`-prefix octal, no
    /// sexagesimal `60:00` integers. YAML 1.1 broadens the resolver
    /// to accept all of those legacy forms.
    ///
    /// Setting this to [`YamlVersion::V1_1`] is equivalent to flipping
    /// every `legacy_*` flag (`legacy_booleans`, `legacy_octal_numbers`,
    /// `legacy_sexagesimal`) on at once. The `legacy_*` flags remain
    /// available for fine-grained overrides — version selection sets a
    /// preset, individual flags refine it.
    pub yaml_version: YamlVersion,
    /// Maximum recursion depth allowed during parsing (default: 128).
    pub max_depth: usize,
    /// Maximum length of a single YAML document in bytes (default: 64 MB).
    pub max_document_length: usize,
    /// Maximum number of times a single anchor can be expanded (default: 1024).
    pub max_alias_expansions: usize,
    /// Maximum number of keys allowed in a single mapping (default: 64k).
    pub max_mapping_keys: usize,
    /// Maximum number of elements allowed in a single sequence (default: 64k).
    pub max_sequence_length: usize,
    /// Maximum total parser events emitted across the input
    /// (default: 1 000 000). Caps event-stream amplification
    /// independent of recursion depth or alias count. Trips
    /// [`crate::Error::Budget`] with
    /// [`crate::BudgetBreach::MaxEvents`].
    pub max_events: usize,
    /// Maximum total `Value` nodes built into the AST
    /// (default: 250 000). Trips
    /// [`crate::BudgetBreach::MaxNodes`].
    pub max_nodes: usize,
    /// Maximum cumulative scalar-byte count across the document
    /// (default: 64 MB). Distinct from
    /// [`Self::max_document_length`] (input size) — this caps
    /// scalar payload after alias expansion. Trips
    /// [`crate::BudgetBreach::MaxTotalScalarBytes`].
    pub max_total_scalar_bytes: usize,
    /// Maximum number of documents in a multi-document stream
    /// (default: 1 000). Trips
    /// [`crate::BudgetBreach::MaxDocuments`].
    pub max_documents: usize,
    /// Maximum number of merge-key (`<<`) entries across the
    /// document (default: 10 000). Trips
    /// [`crate::BudgetBreach::MaxMergeKeys`].
    pub max_merge_keys: usize,
    /// Optional alias-to-anchor ratio heuristic for detecting
    /// billion-laughs amplification patterns
    /// (default: `Some(10.0)`). When more than `ratio × anchors`
    /// aliases have been resolved, the parser trips
    /// [`crate::BudgetBreach::AliasAnchorRatio`]. Set to `None`
    /// to disable.
    pub alias_anchor_ratio: Option<f64>,
    /// How to handle duplicate keys in a mapping (default: Last, per YAML 1.2).
    pub duplicate_key_policy: DuplicateKeyPolicy,
    /// If true, only `true` and `false` (lowercase) are accepted as booleans.
    pub strict_booleans: bool,
    /// If true, accepts YAML 1.1 booleans like `yes`, `no`, `on`, `off`.
    pub legacy_booleans: bool,
    /// Optional registry of custom tags to strip on the streaming path.
    ///
    /// See [`TagRegistry`](crate::TagRegistry) for the full rationale.
    /// `None` (default) preserves the legacy behaviour of routing every
    /// custom-tagged value through the AST fallback.
    pub tag_registry: Option<Arc<crate::TagRegistry>>,
    /// How the YAML merge key (`<<`) should be handled.
    ///
    /// See [`MergeKeyPolicy`] for the available policies. The
    /// default is [`MergeKeyPolicy::Auto`] — the YAML 1.2 spec
    /// behaviour where `<<:` triggers automatic mapping merge.
    pub merge_key_policy: MergeKeyPolicy,
    /// When `true`, plain scalars are *never* resolved to
    /// `null` / `bool` / `int` / `float` — every plain scalar
    /// becomes a string. Useful for schema-strict pipelines that
    /// require the user to quote intent explicitly. Default
    /// `false`.
    pub no_schema: bool,
    /// When `true`, accept YAML 1.1-style bare `0`-prefix octal
    /// literals (e.g. `0644` parsed as 420) in addition to the
    /// YAML 1.2 `0o644` form. Default `false` to honour the YAML
    /// 1.2 schema.
    pub legacy_octal_numbers: bool,
    /// When `true`, deserializing `!!binary "ABCD"` into a
    /// [`String`] target yields the literal base64 source string
    /// (`"ABCD"`) rather than rejecting on tag mismatch. The
    /// canonical bytes path (`Vec<u8>`,
    /// `serde_bytes::ByteBuf`) still decodes the base64 payload
    /// either way. Useful for migrations from Python pyyaml-style
    /// applications that treat the tag as advisory. Default
    /// `false`.
    pub ignore_binary_tag_for_string: bool,
    /// When `true`, accept YAML 1.1-style **sexagesimal** numbers
    /// (`60:00`, `1:30:00`) as integers. The colon-separated
    /// digits are interpreted in base 60: each component is
    /// multiplied by an increasing power of 60, summed left to
    /// right. `60:00` → 3 600; `1:30:00` → 5 400. Negative values
    /// (`-1:30:00`) and partial signs are honoured.
    ///
    /// Off by default to honour the YAML 1.2 schema. Useful for
    /// migrations from YAML 1.1 / Ruby / pyyaml configs that use
    /// the legacy time-of-day notation.
    pub legacy_sexagesimal: bool,
    /// Indentation-validation mode. See [`RequireIndent`].
    /// Default: [`RequireIndent::Unchecked`] — accept any
    /// well-formed YAML indent.
    pub require_indent: RequireIndent,
    /// Pluggable "Safe YAML" policies, run during parsing.
    ///
    /// Each [`Policy`](crate::policy::Policy) inspects parser
    /// events and the post-parse [`Value`](crate::Value) tree; any policy
    /// returning `Err(...)` aborts the parse with that diagnostic.
    /// Empty by default.
    ///
    /// Use [`ParserConfig::with_policy`] to register a policy.
    /// When at least one policy is present the streaming fast-path
    /// is bypassed automatically so the policy contract holds for
    /// every code path.
    pub policies: Vec<Arc<dyn crate::policy::Policy>>,
    /// `${KEY}` / `${KEY:-default}` substitution table consulted
    /// after parsing every document.
    ///
    /// Each scalar in the resulting [`Value`](crate::Value) tree is walked and
    /// any `${name}` placeholder is replaced with the property of
    /// that name. Supported syntax:
    ///
    /// - `${name}` — substitute, error or pass through depending
    ///   on [`Self::strict_properties`]
    /// - `${name:-default}` — substitute, falling back to
    ///   `default` when `name` is missing (always silent, never
    ///   surfaces in errors)
    /// - `${{` — literal `${` (escape for the open delimiter)
    /// - `$$` — literal `$`
    /// - `}}` — literal `}`
    ///
    /// `None` (default) disables the substitution pass entirely;
    /// the parser is unchanged. Setting a non-empty map forces the
    /// AST fallback so the post-parse walk runs uniformly across
    /// every typed target.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub properties: Option<Arc<std::collections::HashMap<String, String>>>,
    /// When `true`, an unknown `${name}` placeholder (no entry in
    /// [`Self::properties`] and no `:-default` fallback) aborts
    /// the parse with [`Error::Custom`](crate::Error::Custom).
    /// When `false` (default), unknown placeholders are replaced
    /// with the empty string — the lossy semantics matching
    /// [`Value::interpolate_properties_lossy`](crate::Value::interpolate_properties_lossy).
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub strict_properties: bool,
    /// `!include` directive resolver. When set, the post-parse
    /// walk substitutes every `Value::Tagged(!include, spec)`
    /// node with the result of `resolver(IncludeRequest)`. See
    /// [`crate::include::IncludeResolver`] for the closure
    /// signature and [`crate::include::SafeFileResolver`] for
    /// the bundled filesystem implementation.
    ///
    /// `None` (default) disables include expansion; tagged
    /// `!include` nodes flow through unchanged.
    #[cfg(feature = "include")]
    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
    pub include_resolver: Option<crate::include::IncludeResolver>,
    /// Maximum `!include` recursion depth. Default 24. Each
    /// nested `!include` increments the depth counter; once the
    /// limit is reached, the parser aborts with
    /// `Error::RecursionLimitExceeded`. Pairs with a per-walk
    /// visited-set to catch cycles independent of depth.
    #[cfg(feature = "include")]
    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
    pub max_include_depth: usize,
}

impl Default for ParserConfig {
    fn default() -> Self {
        ParserConfig {
            yaml_version: YamlVersion::V1_2,
            max_depth: 128,
            max_document_length: 1024 * 1024 * 64, // 64 MB
            max_alias_expansions: 1024,
            max_mapping_keys: 1024 * 64,
            max_sequence_length: 1024 * 64,
            max_events: 1_000_000,
            max_nodes: 250_000,
            max_total_scalar_bytes: 1024 * 1024 * 64, // 64 MB
            max_documents: 1_000,
            max_merge_keys: 10_000,
            alias_anchor_ratio: Some(10.0),
            duplicate_key_policy: DuplicateKeyPolicy::default(),
            strict_booleans: false,
            legacy_booleans: false,
            tag_registry: None,
            merge_key_policy: MergeKeyPolicy::default(),
            no_schema: false,
            legacy_octal_numbers: false,
            ignore_binary_tag_for_string: false,
            legacy_sexagesimal: false,
            require_indent: RequireIndent::Unchecked,
            policies: Vec::new(),
            #[cfg(feature = "std")]
            properties: None,
            #[cfg(feature = "std")]
            strict_properties: false,
            #[cfg(feature = "include")]
            include_resolver: None,
            #[cfg(feature = "include")]
            max_include_depth: 24,
        }
    }
}

impl ParserConfig {
    /// Create a new configuration with default values.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new();
    /// assert_eq!(cfg.max_depth, 128);
    /// ```
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a strict configuration (YAML 1.2 strict) with tighter
    /// security limits suitable for untrusted input.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::strict();
    /// assert_eq!(cfg.max_depth, 64);
    /// ```
    #[must_use]
    pub fn strict() -> Self {
        ParserConfig {
            yaml_version: YamlVersion::V1_2,
            max_depth: 64,
            max_document_length: 1024 * 1024, // 1 MB
            max_alias_expansions: 100,
            max_mapping_keys: 1024,
            max_sequence_length: 1024,
            max_events: 100_000,
            max_nodes: 25_000,
            max_total_scalar_bytes: 1024 * 1024, // 1 MB
            max_documents: 100,
            max_merge_keys: 1_000,
            alias_anchor_ratio: Some(5.0),
            strict_booleans: true,
            legacy_booleans: false,
            duplicate_key_policy: DuplicateKeyPolicy::Error,
            tag_registry: None,
            merge_key_policy: MergeKeyPolicy::default(),
            no_schema: false,
            legacy_octal_numbers: false,
            ignore_binary_tag_for_string: false,
            legacy_sexagesimal: false,
            require_indent: RequireIndent::Even,
            policies: Vec::new(),
            #[cfg(feature = "std")]
            properties: None,
            #[cfg(feature = "std")]
            strict_properties: true,
            #[cfg(feature = "include")]
            include_resolver: None,
            // Strict mode tightens the include recursion ceiling
            // proportionally to its other depth caps (max_depth
            // 128 → 64, max_alias_expansions 1024 → 100).
            #[cfg(feature = "include")]
            max_include_depth: 8,
        }
    }

    /// Install a `${KEY}` substitution table consulted after
    /// parsing.
    ///
    /// Each scalar in the resulting [`Value`](crate::Value) tree is walked and
    /// any `${name}` placeholder is replaced with the property of
    /// that name. Pairs with [`Self::strict_properties`] to choose
    /// between erroring or silently empty-substituting on unknown
    /// keys, and with `${name:-default}` syntax for inline
    /// defaults.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{from_str_with_config, ParserConfig, Value};
    /// use std::collections::HashMap;
    /// use std::sync::Arc;
    ///
    /// let mut props = HashMap::new();
    /// props.insert("HOST".to_string(), "localhost".to_string());
    /// let cfg = ParserConfig::new().properties(Arc::new(props));
    /// let v: Value = from_str_with_config("url: http://${HOST}/", &cfg).unwrap();
    /// assert_eq!(v["url"].as_str(), Some("http://localhost/"));
    /// ```
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    #[must_use]
    pub fn properties(
        mut self,
        properties: Arc<std::collections::HashMap<String, String>>,
    ) -> Self {
        self.properties = Some(properties);
        self
    }

    /// Toggle strict-mode placeholder resolution.
    ///
    /// When `true`, an unknown `${name}` (no map entry, no
    /// `:-default` fallback) aborts the parse. When `false`
    /// (default), unknown placeholders are replaced with the empty
    /// string — useful for environment-style configs where missing
    /// variables should silently degrade.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{from_str_with_config, ParserConfig, Value};
    /// use std::collections::HashMap;
    /// use std::sync::Arc;
    ///
    /// let cfg = ParserConfig::new()
    ///     .properties(Arc::new(HashMap::new()))
    ///     .strict_properties(true);
    /// let res: Result<Value, _> = from_str_with_config("x: ${MISSING}", &cfg);
    /// assert!(res.is_err());
    /// ```
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    #[must_use]
    pub fn strict_properties(mut self, strict: bool) -> Self {
        self.strict_properties = strict;
        self
    }

    /// Install an `!include` directive resolver.
    ///
    /// Each `Value::Tagged(!include, scalar_spec)` node in the
    /// parsed tree is replaced with the resolver's output. The
    /// resolver is consulted with an `IncludeRequest` carrying
    /// the verbatim spec text, a stable source-id, and the
    /// current recursion depth.
    ///
    /// Pair with [`Self::max_include_depth`] to bound the
    /// recursion ceiling. Cycle detection (A includes B includes
    /// A) runs independently using a per-walk visited set.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::include::{IncludeRequest, IncludeResolver, InputSource};
    /// use noyalib::{ParserConfig, Result};
    ///
    /// let resolver = IncludeResolver::new(|req: IncludeRequest<'_>| -> Result<InputSource> {
    ///     // For an in-memory test, fabricate a YAML payload
    ///     // keyed on the spec.
    ///     Ok(InputSource::new(req.spec, format!("name: {}\n", req.spec)))
    /// });
    /// let cfg = ParserConfig::new().include_resolver(resolver);
    /// # let _ = cfg;
    /// ```
    #[cfg(feature = "include")]
    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
    #[must_use]
    pub fn include_resolver(mut self, resolver: crate::include::IncludeResolver) -> Self {
        self.include_resolver = Some(resolver);
        self
    }

    /// Maximum `!include` recursion depth.
    ///
    /// Default 24 (8 in [`Self::strict()`]). Each nested
    /// `!include` increments the depth; once the limit is
    /// reached, the parser aborts with
    /// `Error::RecursionLimitExceeded`. The cap is independent
    /// of [`Self::max_depth`] (which bounds *YAML structural*
    /// nesting) and of the per-walk cycle-detection set (which
    /// catches A→B→A regardless of depth).
    #[cfg(feature = "include")]
    #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
    #[must_use]
    pub fn max_include_depth(mut self, depth: usize) -> Self {
        self.max_include_depth = depth;
        self
    }

    /// Select the YAML specification version the resolver should
    /// honour.
    ///
    /// Selecting [`YamlVersion::V1_1`] is a *preset* over the three
    /// `legacy_*` flags — equivalent to:
    ///
    /// ```text
    /// cfg.legacy_booleans      = true;  // yes / no / on / off
    /// cfg.legacy_octal_numbers = true;  // 0644 → octal
    /// cfg.legacy_sexagesimal   = true;  // 60:00 → 3600
    /// ```
    ///
    /// Selecting [`YamlVersion::V1_2`] resets those three flags to
    /// `false` so callers can revert to strict 1.2 mode without
    /// re-creating the config from scratch. Other fields (limits,
    /// policies, merge-key behaviour) are unaffected.
    ///
    /// Fine-grained overrides (e.g. "1.1 booleans but reject octal
    /// `0644`") work as expected: call `version` first, then flip
    /// individual flags.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
    ///
    /// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
    /// // YAML 1.1 booleans
    /// let v: Value = from_str_with_config("on", &cfg).unwrap();
    /// assert_eq!(v, Value::Bool(true));
    /// // YAML 1.1 octal
    /// let v: Value = from_str_with_config("0644", &cfg).unwrap();
    /// assert_eq!(v, Value::from(420_i64));
    /// // YAML 1.1 sexagesimal
    /// let v: Value = from_str_with_config("1:30", &cfg).unwrap();
    /// assert_eq!(v, Value::from(90_i64));
    /// ```
    #[must_use]
    pub fn version(mut self, version: YamlVersion) -> Self {
        self.yaml_version = version;
        match version {
            YamlVersion::V1_1 => {
                self.legacy_booleans = true;
                self.legacy_octal_numbers = true;
                self.legacy_sexagesimal = true;
            }
            YamlVersion::V1_2 => {
                self.legacy_booleans = false;
                self.legacy_octal_numbers = false;
                self.legacy_sexagesimal = false;
            }
        }
        self
    }

    /// Register a [`Policy`](crate::policy::Policy) to enforce
    /// during parsing.
    ///
    /// Multiple policies may be registered; they all run in
    /// registration order, and the first error short-circuits the
    /// parse. When any policy is present the streaming fast-path
    /// is bypassed so the policy contract is enforced uniformly.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{from_str_with_config, ParserConfig, Value};
    /// use noyalib::policy::DenyAnchors;
    ///
    /// let cfg = ParserConfig::new().with_policy(DenyAnchors);
    /// let res: Result<Value, _> =
    ///     from_str_with_config("a: &x 1\nb: *x\n", &cfg);
    /// assert!(res.is_err());
    /// ```
    #[must_use]
    pub fn with_policy<P>(mut self, policy: P) -> Self
    where
        P: crate::policy::Policy + 'static,
    {
        self.policies.push(Arc::new(policy));
        self
    }

    /// Set the maximum recursion depth.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_depth(32);
    /// assert_eq!(cfg.max_depth, 32);
    /// ```
    #[must_use]
    pub fn max_depth(mut self, depth: usize) -> Self {
        self.max_depth = depth;
        self
    }

    /// Set the maximum document length.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_document_length(1024);
    /// assert_eq!(cfg.max_document_length, 1024);
    /// ```
    #[must_use]
    pub fn max_document_length(mut self, len: usize) -> Self {
        self.max_document_length = len;
        self
    }

    /// Set the maximum alias expansions.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_alias_expansions(50);
    /// assert_eq!(cfg.max_alias_expansions, 50);
    /// ```
    #[must_use]
    pub fn max_alias_expansions(mut self, expansions: usize) -> Self {
        self.max_alias_expansions = expansions;
        self
    }

    /// Set the maximum number of mapping keys.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_mapping_keys(100);
    /// assert_eq!(cfg.max_mapping_keys, 100);
    /// ```
    #[must_use]
    pub fn max_mapping_keys(mut self, max: usize) -> Self {
        self.max_mapping_keys = max;
        self
    }

    /// Set the maximum sequence length.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_sequence_length(100);
    /// assert_eq!(cfg.max_sequence_length, 100);
    /// ```
    #[must_use]
    pub fn max_sequence_length(mut self, max: usize) -> Self {
        self.max_sequence_length = max;
        self
    }

    /// Set the maximum total parser-event budget.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_events(50_000);
    /// assert_eq!(cfg.max_events, 50_000);
    /// ```
    #[must_use]
    pub fn max_events(mut self, max: usize) -> Self {
        self.max_events = max;
        self
    }

    /// Set the maximum total `Value` node budget.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_nodes(10_000);
    /// assert_eq!(cfg.max_nodes, 10_000);
    /// ```
    #[must_use]
    pub fn max_nodes(mut self, max: usize) -> Self {
        self.max_nodes = max;
        self
    }

    /// Set the maximum cumulative scalar-byte budget.
    ///
    /// Distinct from [`Self::max_document_length`] — this caps
    /// scalar bytes after alias expansion.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_total_scalar_bytes(8 * 1024 * 1024);
    /// assert_eq!(cfg.max_total_scalar_bytes, 8 * 1024 * 1024);
    /// ```
    #[must_use]
    pub fn max_total_scalar_bytes(mut self, max: usize) -> Self {
        self.max_total_scalar_bytes = max;
        self
    }

    /// Set the maximum document count for multi-document streams.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_documents(64);
    /// assert_eq!(cfg.max_documents, 64);
    /// ```
    #[must_use]
    pub fn max_documents(mut self, max: usize) -> Self {
        self.max_documents = max;
        self
    }

    /// Set the maximum merge-key count budget.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().max_merge_keys(1_000);
    /// assert_eq!(cfg.max_merge_keys, 1_000);
    /// ```
    #[must_use]
    pub fn max_merge_keys(mut self, max: usize) -> Self {
        self.max_merge_keys = max;
        self
    }

    /// Set the indentation-validation mode.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{ParserConfig, RequireIndent};
    /// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
    /// assert_eq!(cfg.require_indent, RequireIndent::Even);
    /// ```
    #[must_use]
    pub fn require_indent(mut self, mode: RequireIndent) -> Self {
        self.require_indent = mode;
        self
    }

    /// Set the alias-to-anchor ratio heuristic.
    ///
    /// Pass `Some(ratio)` to enable the billion-laughs guard,
    /// `None` to disable.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().alias_anchor_ratio(Some(20.0));
    /// assert_eq!(cfg.alias_anchor_ratio, Some(20.0));
    /// ```
    #[must_use]
    pub fn alias_anchor_ratio(mut self, ratio: Option<f64>) -> Self {
        self.alias_anchor_ratio = ratio;
        self
    }

    /// Set the duplicate key policy.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{DuplicateKeyPolicy, ParserConfig};
    /// let cfg = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
    /// assert_eq!(cfg.duplicate_key_policy, DuplicateKeyPolicy::Error);
    /// ```
    #[must_use]
    pub fn duplicate_key_policy(mut self, policy: DuplicateKeyPolicy) -> Self {
        self.duplicate_key_policy = policy;
        self
    }

    /// Enable or disable strict booleans.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().strict_booleans(true);
    /// assert!(cfg.strict_booleans);
    /// ```
    #[must_use]
    pub fn strict_booleans(mut self, strict: bool) -> Self {
        self.strict_booleans = strict;
        self
    }

    /// Enable or disable legacy booleans.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().legacy_booleans(true);
    /// assert!(cfg.legacy_booleans);
    /// ```
    #[must_use]
    pub fn legacy_booleans(mut self, legacy: bool) -> Self {
        self.legacy_booleans = legacy;
        self
    }

    /// Attach a [`TagRegistry`](crate::TagRegistry) so the streaming
    /// deserializer strips listed custom tags instead of routing them
    /// through the AST.
    ///
    /// See the [`tag_registry`](crate::tag_registry) module
    /// documentation for when to use this versus `#[serde(rename)]`.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{ParserConfig, TagRegistry};
    /// use std::sync::Arc;
    /// let reg = Arc::new(TagRegistry::new().with("!Celsius"));
    /// let cfg = ParserConfig::new().tag_registry(Arc::clone(&reg));
    /// assert!(cfg.tag_registry.is_some());
    /// ```
    #[must_use]
    pub fn tag_registry(mut self, registry: Arc<crate::TagRegistry>) -> Self {
        self.tag_registry = Some(registry);
        self
    }

    /// Set the policy for handling the YAML merge key (`<<`).
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::{MergeKeyPolicy, ParserConfig};
    /// let cfg = ParserConfig::new().merge_key_policy(MergeKeyPolicy::AsOrdinary);
    /// assert_eq!(cfg.merge_key_policy, MergeKeyPolicy::AsOrdinary);
    /// ```
    #[must_use]
    pub fn merge_key_policy(mut self, policy: MergeKeyPolicy) -> Self {
        self.merge_key_policy = policy;
        self
    }

    /// Toggle schema-free plain-scalar resolution. When `true`,
    /// every plain scalar becomes a string regardless of whether
    /// it would normally resolve to `null`, `bool`, integer, or
    /// float.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().no_schema(true);
    /// assert!(cfg.no_schema);
    /// ```
    #[must_use]
    pub fn no_schema(mut self, no_schema: bool) -> Self {
        self.no_schema = no_schema;
        self
    }

    /// Toggle YAML 1.1-style bare `0`-prefix octal parsing
    /// (e.g. `0644` → 420). Off by default; YAML 1.2 requires the
    /// `0o` prefix.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().legacy_octal_numbers(true);
    /// assert!(cfg.legacy_octal_numbers);
    /// ```
    #[must_use]
    pub fn legacy_octal_numbers(mut self, on: bool) -> Self {
        self.legacy_octal_numbers = on;
        self
    }

    /// Toggle the migration-helper behaviour where
    /// `!!binary "ABCD"` deserializes into a [`String`] target as
    /// the literal base64 source string. The canonical bytes
    /// path (`Vec<u8>`, `serde_bytes::ByteBuf`) is unaffected —
    /// it always decodes the base64 payload.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().ignore_binary_tag_for_string(true);
    /// assert!(cfg.ignore_binary_tag_for_string);
    /// ```
    #[must_use]
    pub fn ignore_binary_tag_for_string(mut self, on: bool) -> Self {
        self.ignore_binary_tag_for_string = on;
        self
    }

    /// Toggle YAML 1.1-style sexagesimal number parsing
    /// (`60:00` → 3 600). Off by default; YAML 1.2 dropped the
    /// sexagesimal schema, so plain `1:30:00` would otherwise
    /// surface as a string.
    ///
    /// # Examples
    ///
    /// ```
    /// use noyalib::ParserConfig;
    /// let cfg = ParserConfig::new().legacy_sexagesimal(true);
    /// assert!(cfg.legacy_sexagesimal);
    /// ```
    #[must_use]
    pub fn legacy_sexagesimal(mut self, on: bool) -> Self {
        self.legacy_sexagesimal = on;
        self
    }
}

/// Policy for handling the YAML merge key (`<<`) during parsing.
///
/// YAML 1.2 §10.2 defines `<<` as a "merge key" that, when used as
/// a mapping key, splices the value's mapping (or sequence of
/// mappings) into the enclosing mapping. The variants below let
/// callers opt out of that behaviour.
///
/// # Examples
///
/// Indentation-validation mode for the YAML scanner.
///
/// Issue #6 surface — every block-context indent transition is
/// classified per the chosen mode. The default
/// ([`RequireIndent::Unchecked`]) accepts any well-formed YAML
/// indent (the YAML 1.2 spec mandate), which is what every
/// other Rust YAML parser does.
///
/// Stricter modes are useful in pipelines that require uniform
/// indentation house style (linters, formatters, reviewer
/// gates) — a config file with mixed `2`-space and `4`-space
/// indent passes by-spec but fails consistency review.
///
/// # Variants
///
/// - [`RequireIndent::Unchecked`] (default) — by-spec mode.
/// - [`RequireIndent::Even`] — every indent delta must be even.
/// - `RequireIndent::Divisible(N)` — every indent delta must
///   be divisible by `N`.
/// - `RequireIndent::Uniform(Some(N))` — every indent delta
///   must equal `N`. `None` means "auto-detect from the first
///   delta and require the rest to match it".
///
/// # Examples
///
/// ```
/// use noyalib::{ParserConfig, RequireIndent};
/// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
/// assert_eq!(cfg.require_indent, RequireIndent::Even);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum RequireIndent {
    /// Accept any indent transition the YAML 1.2 spec allows.
    /// Default.
    #[default]
    Unchecked,
    /// Indent delta must be even (`2`, `4`, `6`, …). The most
    /// common house-style.
    Even,
    /// Indent delta must be divisible by `N`.
    Divisible(usize),
    /// `Some(N)`: every indent delta must equal `N`.
    /// `None`: the first delta sets the standard for the
    /// document; subsequent deltas must match it.
    Uniform(Option<usize>),
}

/// ```
/// use noyalib::MergeKeyPolicy;
/// assert_eq!(MergeKeyPolicy::default(), MergeKeyPolicy::Auto);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MergeKeyPolicy {
    /// Apply the YAML 1.2 merge-key semantics — `<<:` keys trigger
    /// automatic merge of the value into the enclosing mapping.
    /// Default.
    #[default]
    Auto,
    /// Treat `<<` as an ordinary string key. The mapping retains a
    /// literal `<<` entry whose value is whatever the YAML
    /// document supplied. Useful when round-tripping configuration
    /// files that happen to contain a `<<` key for non-merge
    /// reasons.
    AsOrdinary,
    /// Reject any document that contains a `<<` key with
    /// [`crate::Error::Custom`]. Useful for schema-strict pipelines
    /// where merge keys are forbidden.
    Error,
}

/// Policy for handling duplicate keys in a YAML mapping.
///
/// # Examples
///
/// ```
/// use noyalib::DuplicateKeyPolicy;
/// assert_eq!(DuplicateKeyPolicy::default(), DuplicateKeyPolicy::Last);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DuplicateKeyPolicy {
    /// Use the first occurrence of the key; ignore subsequent ones.
    First,
    /// Use the last occurrence of the key (YAML 1.2 default).
    #[default]
    Last,
    /// Return an error if a duplicate key is encountered.
    Error,
}