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/// Stable parser configuration presets.
46///
47/// Profiles select a complete semantic and resource-limit baseline. Use
48/// [`ParserConfig::profile`] when configuration must be named explicitly,
49/// such as at a service trust boundary. Callers can still refine individual
50/// settings with the existing builder methods.
51///
52/// # Examples
53///
54/// ```
55/// use noyalib::{ParserConfig, ParserProfile};
56///
57/// let cfg = ParserConfig::profile(ParserProfile::Strict);
58/// assert_eq!(cfg.max_depth, 64);
59/// ```
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61#[non_exhaustive]
62pub enum ParserProfile {
63 /// YAML 1.2 semantics with the standard resource budgets.
64 #[default]
65 Standard,
66 /// YAML 1.2 semantics with tighter budgets and validation for untrusted
67 /// input.
68 Strict,
69 /// Compatibility semantics matching `serde_yaml` 0.9.
70 ///
71 /// This profile retains the standard resource budgets. It is a migration
72 /// profile, not a hardened-input profile.
73 SerdeYaml,
74}
75
76/// Resource budgets enforced while parsing YAML.
77///
78/// This value separates denial-of-service controls from YAML semantics and
79/// post-parse integrations. Apply a complete budget set with
80/// [`ParserConfig::with_limits`], or read the active budgets with
81/// [`ParserConfig::limits`]. Existing `ParserConfig` limit fields remain
82/// public for backward compatibility.
83///
84/// # Examples
85///
86/// ```
87/// use noyalib::{ParserConfig, ParserLimits};
88///
89/// let mut limits = ParserLimits::strict();
90/// limits.max_document_length = 8 * 1024;
91/// let cfg = ParserConfig::new().with_limits(limits);
92/// assert_eq!(cfg.max_document_length, 8 * 1024);
93/// ```
94#[derive(Debug, Clone, Copy, PartialEq)]
95#[non_exhaustive]
96pub struct ParserLimits {
97 /// Maximum YAML structural recursion depth.
98 pub max_depth: usize,
99 /// Maximum bytes in one YAML document.
100 pub max_document_length: usize,
101 /// Maximum bytes in one multi-document stream.
102 pub max_stream_bytes: usize,
103 /// Maximum expansions of one anchor.
104 pub max_alias_expansions: usize,
105 /// Maximum keys in one mapping.
106 pub max_mapping_keys: usize,
107 /// Maximum elements in one sequence.
108 pub max_sequence_length: usize,
109 /// Maximum parser events across the input.
110 pub max_events: usize,
111 /// Maximum `Value` nodes across the input.
112 pub max_nodes: usize,
113 /// Maximum scalar bytes after alias expansion.
114 pub max_total_scalar_bytes: usize,
115 /// Maximum documents in one stream.
116 pub max_documents: usize,
117 /// Maximum merge-key entries across one document.
118 pub max_merge_keys: usize,
119 /// Optional maximum alias-to-anchor ratio.
120 pub alias_anchor_ratio: Option<f64>,
121 /// Optional transitive alias-expansion factor per parser event.
122 pub alias_jump_event_factor: Option<usize>,
123 /// Maximum `!include` recursion depth.
124 #[cfg(feature = "include")]
125 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
126 pub max_include_depth: usize,
127 /// Maximum sources resolved by one include walk.
128 #[cfg(feature = "include")]
129 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
130 pub max_include_sources: usize,
131 /// Maximum cumulative bytes returned by include resolvers.
132 #[cfg(feature = "include")]
133 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
134 pub max_total_include_bytes: usize,
135}
136
137impl Default for ParserLimits {
138 fn default() -> Self {
139 Self {
140 max_depth: 128,
141 max_document_length: 64 * 1024 * 1024,
142 max_stream_bytes: 256 * 1024 * 1024,
143 max_alias_expansions: 1_024,
144 max_mapping_keys: 64 * 1024,
145 max_sequence_length: 64 * 1024,
146 max_events: 1_000_000,
147 max_nodes: 250_000,
148 max_total_scalar_bytes: 64 * 1024 * 1024,
149 max_documents: 1_000,
150 max_merge_keys: 10_000,
151 alias_anchor_ratio: Some(10.0),
152 alias_jump_event_factor: None,
153 #[cfg(feature = "include")]
154 max_include_depth: 24,
155 #[cfg(feature = "include")]
156 max_include_sources: 256,
157 #[cfg(feature = "include")]
158 max_total_include_bytes: 64 * 1024 * 1024,
159 }
160 }
161}
162
163impl ParserLimits {
164 /// Return the resource budgets associated with a named parser profile.
165 ///
166 /// The `SerdeYaml` profile uses the standard absolute budgets plus the
167 /// compatibility layer's transitive alias-expansion factor.
168 #[must_use]
169 pub fn profile(profile: ParserProfile) -> Self {
170 match profile {
171 ParserProfile::Standard => Self::default(),
172 ParserProfile::Strict => Self::strict(),
173 ParserProfile::SerdeYaml => Self {
174 alias_jump_event_factor: Some(100),
175 ..Self::default()
176 },
177 }
178 }
179
180 /// Return the tighter budgets used by [`ParserProfile::Strict`].
181 #[must_use]
182 pub const fn strict() -> Self {
183 Self {
184 max_depth: 64,
185 max_document_length: 1024 * 1024,
186 max_stream_bytes: 16 * 1024 * 1024,
187 max_alias_expansions: 100,
188 max_mapping_keys: 1_024,
189 max_sequence_length: 1_024,
190 max_events: 100_000,
191 max_nodes: 25_000,
192 max_total_scalar_bytes: 1024 * 1024,
193 max_documents: 100,
194 max_merge_keys: 1_000,
195 alias_anchor_ratio: Some(5.0),
196 alias_jump_event_factor: None,
197 #[cfg(feature = "include")]
198 max_include_depth: 8,
199 #[cfg(feature = "include")]
200 max_include_sources: 32,
201 #[cfg(feature = "include")]
202 max_total_include_bytes: 1024 * 1024,
203 }
204 }
205}
206
207/// Deserialization configuration.
208///
209/// All fields are public, but the struct is annotated
210/// [`#[non_exhaustive]`][nex] so that adding a new budget or
211/// policy in a future minor release is **not** a breaking change.
212/// Construct with [`ParserConfig::new`] / [`ParserConfig::strict`]
213/// / [`ParserConfig::default`] (preferred) or with the
214/// `..ParserConfig::default()` struct-update form; do not
215/// construct from an exhaustive struct-literal outside this
216/// crate.
217///
218/// [nex]: https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute
219///
220/// # Examples
221///
222/// ```
223/// use noyalib::ParserConfig;
224/// let cfg = ParserConfig::new().max_depth(64);
225/// assert_eq!(cfg.max_depth, 64);
226/// ```
227#[derive(Debug, Clone)]
228#[non_exhaustive]
229pub struct ParserConfig {
230 /// Which YAML specification version to honour during plain-scalar
231 /// resolution.
232 ///
233 /// YAML 1.2 (default) follows the **core schema** — strict
234 /// `true`/`false` booleans, no bare `0`-prefix octal, no
235 /// sexagesimal `60:00` integers. YAML 1.1 broadens the resolver
236 /// to accept all of those legacy forms.
237 ///
238 /// Setting this to [`YamlVersion::V1_1`] is equivalent to flipping
239 /// every `legacy_*` flag (`legacy_booleans`, `legacy_octal_numbers`,
240 /// `legacy_sexagesimal`) on at once. The `legacy_*` flags remain
241 /// available for fine-grained overrides — version selection sets a
242 /// preset, individual flags refine it.
243 pub yaml_version: YamlVersion,
244 /// Maximum recursion depth allowed during parsing (default: 128).
245 pub max_depth: usize,
246 /// Maximum length of a single YAML document in bytes (default: 64 MB).
247 pub max_document_length: usize,
248 /// Maximum total byte length of a multi-document stream
249 /// (default: 256 MB). Async multi-document readers enforce this
250 /// before splitting, while each document remains subject to
251 /// [`Self::max_document_length`].
252 pub max_stream_bytes: usize,
253 /// Maximum number of times a single anchor can be expanded (default: 1024).
254 pub max_alias_expansions: usize,
255 /// Maximum number of keys allowed in a single mapping (default: 64k).
256 pub max_mapping_keys: usize,
257 /// Maximum number of elements allowed in a single sequence (default: 64k).
258 pub max_sequence_length: usize,
259 /// Maximum total parser events emitted across the input
260 /// (default: 1 000 000). Caps event-stream amplification
261 /// independent of recursion depth or alias count. Trips
262 /// [`crate::Error::Budget`] with
263 /// [`crate::BudgetBreach::MaxEvents`].
264 pub max_events: usize,
265 /// Maximum total `Value` nodes authored into the AST across the
266 /// input (default: 250 000). Each scalar, sequence, and mapping —
267 /// empty collections included — counts as one node, so this bounds
268 /// node-dense payloads (long runs of `[]`/`{}`) that stay under the
269 /// scalar-byte and event caps. Trips [`crate::Error::Budget`] with
270 /// [`crate::BudgetBreach::MaxNodes`]. Enforced on the AST-loader
271 /// path and re-applied to the fully expanded include graph; raise it
272 /// for deliberately large documents.
273 pub max_nodes: usize,
274 /// Maximum cumulative scalar-byte count across the document
275 /// (default: 64 MB). Distinct from
276 /// [`Self::max_document_length`] (input size) — this caps
277 /// scalar payload after alias expansion. Trips
278 /// [`crate::BudgetBreach::MaxTotalScalarBytes`].
279 pub max_total_scalar_bytes: usize,
280 /// Maximum number of documents in a multi-document stream
281 /// (default: 1 000). Trips
282 /// [`crate::BudgetBreach::MaxDocuments`].
283 pub max_documents: usize,
284 /// Maximum number of merge-key (`<<`) entries across the
285 /// document (default: 10 000). Trips
286 /// [`crate::BudgetBreach::MaxMergeKeys`].
287 pub max_merge_keys: usize,
288 /// Optional alias-to-anchor ratio heuristic for detecting
289 /// billion-laughs amplification patterns
290 /// (default: `Some(10.0)`). When more than `ratio × anchors`
291 /// aliases have been resolved, the parser trips
292 /// [`crate::BudgetBreach::AliasAnchorRatio`]. Set to `None`
293 /// to disable.
294 pub alias_anchor_ratio: Option<f64>,
295 /// How to handle duplicate keys in a mapping (default: Last, per YAML 1.2).
296 pub duplicate_key_policy: DuplicateKeyPolicy,
297 /// If true, only `true` and `false` (lowercase) are accepted as booleans.
298 pub strict_booleans: bool,
299 /// If true, accepts YAML 1.1 booleans like `yes`, `no`, `on`, `off`.
300 pub legacy_booleans: bool,
301 /// Optional registry of custom tags to strip on the streaming path.
302 ///
303 /// See [`TagRegistry`](crate::TagRegistry) for the full rationale.
304 /// `None` (default) preserves the legacy behaviour of routing every
305 /// custom-tagged value through the AST fallback.
306 pub tag_registry: Option<Arc<crate::TagRegistry>>,
307 /// How the YAML merge key (`<<`) should be handled.
308 ///
309 /// See [`MergeKeyPolicy`] for the available policies. The
310 /// default is [`MergeKeyPolicy::Auto`] — the YAML 1.2 spec
311 /// behaviour where `<<:` triggers automatic mapping merge.
312 pub merge_key_policy: MergeKeyPolicy,
313 /// When `true`, plain scalars are *never* resolved to
314 /// `null` / `bool` / `int` / `float` — every plain scalar
315 /// becomes a string. Useful for schema-strict pipelines that
316 /// require the user to quote intent explicitly. Default
317 /// `false`.
318 pub no_schema: bool,
319 /// When `true`, accept YAML 1.1-style bare `0`-prefix octal
320 /// literals (e.g. `0644` parsed as 420) in addition to the
321 /// YAML 1.2 `0o644` form. Default `false` to honour the YAML
322 /// 1.2 schema.
323 pub legacy_octal_numbers: bool,
324 /// When `true`, deserializing `!!binary "ABCD"` into a
325 /// [`String`] target yields the literal base64 source string
326 /// (`"ABCD"`) rather than rejecting on tag mismatch. The
327 /// canonical bytes path (`Vec<u8>`,
328 /// `serde_bytes::ByteBuf`) still decodes the base64 payload
329 /// either way. Useful for migrations from Python pyyaml-style
330 /// applications that treat the tag as advisory. Default
331 /// `false`.
332 pub ignore_binary_tag_for_string: bool,
333 /// When enabled, a plain scalar deserializes into a [`String`]
334 /// (or `char`) target as its source text even where the YAML 1.2
335 /// schema resolves it to a number, boolean, or null:
336 /// `password: 123456` gives `"123456"`, `~` gives `"~"`, an empty
337 /// value gives `""`. Off by default: a `String` field refuses a
338 /// non-string plain scalar. Quoted scalars are strings either
339 /// way. Matches what serde_yaml did for typed targets.
340 pub plain_scalar_strings: bool,
341 /// When `true`, accept YAML 1.1-style **sexagesimal** numbers
342 /// (`60:00`, `1:30:00`) as integers. The colon-separated
343 /// digits are interpreted in base 60: each component is
344 /// multiplied by an increasing power of 60, summed left to
345 /// right. `60:00` → 3 600; `1:30:00` → 5 400. Negative values
346 /// (`-1:30:00`) and partial signs are honoured.
347 ///
348 /// Off by default to honour the YAML 1.2 schema. Useful for
349 /// migrations from YAML 1.1 / Ruby / pyyaml configs that use
350 /// the legacy time-of-day notation.
351 pub legacy_sexagesimal: bool,
352 /// When `true`, and the `lossless-u64` Cargo feature is enabled,
353 /// YAML integer scalars in `(i64::MAX, u64::MAX]` resolve as
354 /// unsigned integers instead of falling through to `f64`.
355 ///
356 /// Default `false` to preserve the historical public
357 /// `Integer(i64)` / `Float(f64)` model and serde-yaml compatibility.
358 #[cfg(feature = "lossless-u64")]
359 #[cfg_attr(docsrs, doc(cfg(feature = "lossless-u64")))]
360 pub lossless_u64_integers: bool,
361 /// When `true`, a plain decimal integer with a leading zero
362 /// (`0123`, `+007`) resolves as a **string** instead of a
363 /// number. This is what `serde_yaml` 0.9 (libyaml) did — the
364 /// spelling is octal in YAML 1.1 and decimal in 1.2, and
365 /// libyaml sidestepped the ambiguity by resolving neither.
366 /// `0`, `0o755`, and `0x1F` are unaffected. Default `false`
367 /// (YAML 1.2: a decimal integer).
368 pub leading_zero_integer_strings: bool,
369 /// When `true`, accept YAML 1.1-style binary literals
370 /// (`0b11` → 3). Default `false` to honour the YAML 1.2
371 /// schema, which has no binary form.
372 pub legacy_binary_numbers: bool,
373 /// When `true`, a plain scalar float whose *literal* spelling
374 /// overflows `f64` (`1e999`) resolves as a **string** instead
375 /// of infinity. The explicit spellings `.inf` / `-.Inf` /
376 /// `.nan` still resolve to their float values. Matches
377 /// `serde_yaml` 0.9. Default `false` (overflow saturates to
378 /// infinity, the Rust float-parsing convention).
379 pub float_overflow_strings: bool,
380 /// When `true`, a plain decimal integer beyond `u64::MAX`
381 /// (`18446744073709551616`) aborts the parse with
382 /// [`ErrorKind::IntegerOverflow`](crate::ErrorKind) instead
383 /// of falling through to an approximate `f64`. Matches
384 /// `serde_yaml` 0.9's "JSON number out of range". Default
385 /// `false`.
386 pub integer_overflow_errors: bool,
387 /// What to do with a non-scalar mapping key (`[a, b]: v`,
388 /// `{k: v}: w`). See [`NonScalarKeyPolicy`]. Default
389 /// [`NonScalarKeyPolicy::Stringify`] — the key is converted
390 /// to its deterministic string form.
391 pub non_scalar_key_policy: NonScalarKeyPolicy,
392 /// Transitive alias-expansion budget as a multiple of the
393 /// document's own event count, mirroring `serde_yaml` 0.9's
394 /// rule (its deserializer refuses once alias jumps exceed
395 /// `events × 100` with "repetition limit exceeded"). Each
396 /// alias expansion charges the full node count of the anchored
397 /// subtree, so nested anchors multiply. `None` (default)
398 /// disables the factor check; the absolute
399 /// [`Self::max_alias_expansions`] and byte budgets still
400 /// apply.
401 pub alias_jump_event_factor: Option<usize>,
402 /// Indentation-validation mode. See [`RequireIndent`].
403 /// Default: [`RequireIndent::Unchecked`] — accept any
404 /// well-formed YAML indent.
405 pub require_indent: RequireIndent,
406 /// Pluggable "Safe YAML" policies, run during parsing.
407 ///
408 /// Each [`Policy`](crate::policy::Policy) inspects parser
409 /// events and the post-parse [`Value`](crate::Value) tree; any policy
410 /// returning `Err(...)` aborts the parse with that diagnostic.
411 /// Empty by default.
412 ///
413 /// Use [`ParserConfig::with_policy`] to register a policy.
414 /// When at least one policy is present the streaming fast-path
415 /// is bypassed automatically so the policy contract holds for
416 /// every code path.
417 pub policies: Vec<Arc<dyn crate::policy::Policy>>,
418 /// `${KEY}` / `${KEY:-default}` substitution table consulted
419 /// after parsing every document.
420 ///
421 /// Each scalar in the resulting [`Value`](crate::Value) tree is walked and
422 /// any `${name}` placeholder is replaced with the property of
423 /// that name. Supported syntax:
424 ///
425 /// - `${name}` — substitute, error or pass through depending
426 /// on [`Self::strict_properties`]
427 /// - `${name:-default}` — substitute, falling back to
428 /// `default` when `name` is missing (always silent, never
429 /// surfaces in errors)
430 /// - `${{` — literal `${` (escape for the open delimiter)
431 /// - `$$` — literal `$`
432 /// - `}}` — literal `}`
433 ///
434 /// `None` (default) disables the substitution pass entirely;
435 /// the parser is unchanged. Setting a non-empty map forces the
436 /// AST fallback so the post-parse walk runs uniformly across
437 /// every typed target.
438 #[cfg(feature = "std")]
439 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
440 pub properties: Option<Arc<std::collections::HashMap<String, String>>>,
441 /// When `true`, an unknown `${name}` placeholder (no entry in
442 /// [`Self::properties`] and no `:-default` fallback) aborts
443 /// the parse with [`Error::Custom`](crate::Error::Custom).
444 /// When `false` (default), unknown placeholders are replaced
445 /// with the empty string — the lossy semantics matching
446 /// [`Value::interpolate_properties_lossy`](crate::Value::interpolate_properties_lossy).
447 #[cfg(feature = "std")]
448 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
449 pub strict_properties: bool,
450 /// `!include` directive resolver. When set, the post-parse
451 /// walk substitutes every `Value::Tagged(!include, spec)`
452 /// node with the result of `resolver(IncludeRequest)`. See
453 /// [`crate::include::IncludeResolver`] for the closure
454 /// signature and [`crate::include::SafeFileResolver`] for
455 /// the bundled filesystem implementation.
456 ///
457 /// `None` (default) disables include expansion; tagged
458 /// `!include` nodes flow through unchanged.
459 #[cfg(feature = "include")]
460 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
461 pub include_resolver: Option<crate::include::IncludeResolver>,
462 /// Maximum `!include` recursion depth. Default 24. Each
463 /// nested `!include` increments the depth counter; once the
464 /// limit is reached, the parser aborts with
465 /// `Error::RecursionLimitExceeded`. Pairs with a per-walk
466 /// visited-set to catch cycles independent of depth.
467 #[cfg(feature = "include")]
468 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
469 pub max_include_depth: usize,
470 /// Maximum number of sources resolved during one include walk.
471 /// Default 256.
472 #[cfg(feature = "include")]
473 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
474 pub max_include_sources: usize,
475 /// Maximum cumulative bytes returned by include resolvers during one
476 /// include walk. Default 64 MiB.
477 #[cfg(feature = "include")]
478 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
479 pub max_total_include_bytes: usize,
480 /// Stream-wide budget counters, attached by the multi-document entry
481 /// points that parse each document separately so the stream-wide
482 /// budgets keep their meaning. Never set by callers.
483 pub(crate) stream_tally: Option<Arc<crate::parser::meter::StreamTally>>,
484}
485
486impl Default for ParserConfig {
487 fn default() -> Self {
488 let limits = ParserLimits::default();
489 Self {
490 yaml_version: YamlVersion::V1_2,
491 max_depth: limits.max_depth,
492 max_document_length: limits.max_document_length,
493 max_stream_bytes: limits.max_stream_bytes,
494 max_alias_expansions: limits.max_alias_expansions,
495 max_mapping_keys: limits.max_mapping_keys,
496 max_sequence_length: limits.max_sequence_length,
497 max_events: limits.max_events,
498 max_nodes: limits.max_nodes,
499 max_total_scalar_bytes: limits.max_total_scalar_bytes,
500 max_documents: limits.max_documents,
501 max_merge_keys: limits.max_merge_keys,
502 alias_anchor_ratio: limits.alias_anchor_ratio,
503 duplicate_key_policy: DuplicateKeyPolicy::default(),
504 strict_booleans: false,
505 legacy_booleans: false,
506 tag_registry: None,
507 merge_key_policy: MergeKeyPolicy::default(),
508 no_schema: false,
509 legacy_octal_numbers: false,
510 ignore_binary_tag_for_string: false,
511 plain_scalar_strings: false,
512 legacy_sexagesimal: false,
513 #[cfg(feature = "lossless-u64")]
514 lossless_u64_integers: false,
515 leading_zero_integer_strings: false,
516 legacy_binary_numbers: false,
517 float_overflow_strings: false,
518 integer_overflow_errors: false,
519 non_scalar_key_policy: NonScalarKeyPolicy::Stringify,
520 alias_jump_event_factor: limits.alias_jump_event_factor,
521 require_indent: RequireIndent::Unchecked,
522 policies: Vec::new(),
523 #[cfg(feature = "std")]
524 properties: None,
525 #[cfg(feature = "std")]
526 strict_properties: false,
527 #[cfg(feature = "include")]
528 include_resolver: None,
529 #[cfg(feature = "include")]
530 max_include_depth: limits.max_include_depth,
531 #[cfg(feature = "include")]
532 max_include_sources: limits.max_include_sources,
533 #[cfg(feature = "include")]
534 max_total_include_bytes: limits.max_total_include_bytes,
535 stream_tally: None,
536 }
537 }
538}
539
540impl ParserConfig {
541 /// Create a new configuration with default values.
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// use noyalib::ParserConfig;
547 /// let cfg = ParserConfig::new();
548 /// assert_eq!(cfg.max_depth, 128);
549 /// ```
550 #[must_use]
551 pub fn new() -> Self {
552 Self::default()
553 }
554
555 /// Create a configuration from a stable named profile.
556 ///
557 /// Profiles make trust-boundary configuration explicit and keep callers
558 /// independent from the individual fields that compose each baseline.
559 /// Builder methods can refine the returned configuration.
560 ///
561 /// # Examples
562 ///
563 /// ```
564 /// use noyalib::{ParserConfig, ParserProfile};
565 ///
566 /// let cfg = ParserConfig::profile(ParserProfile::Strict)
567 /// .max_document_length(32 * 1024);
568 /// assert_eq!(cfg.max_document_length, 32 * 1024);
569 /// ```
570 #[must_use]
571 pub fn profile(profile: ParserProfile) -> Self {
572 match profile {
573 ParserProfile::Standard => Self::default(),
574 ParserProfile::Strict => Self::strict(),
575 ParserProfile::SerdeYaml => Self::serde_yaml_compat(),
576 }
577 }
578
579 /// Create a strict configuration (YAML 1.2 strict) with tighter
580 /// security limits suitable for untrusted input.
581 ///
582 /// # Examples
583 ///
584 /// ```
585 /// use noyalib::ParserConfig;
586 /// let cfg = ParserConfig::strict();
587 /// assert_eq!(cfg.max_depth, 64);
588 /// ```
589 #[must_use]
590 pub fn strict() -> Self {
591 Self {
592 strict_booleans: true,
593 duplicate_key_policy: DuplicateKeyPolicy::Error,
594 require_indent: RequireIndent::Even,
595 #[cfg(feature = "std")]
596 strict_properties: true,
597 ..Self::default().with_limits(ParserLimits::profile(ParserProfile::Strict))
598 }
599 }
600
601 /// Create the configuration the `compat-serde-yaml` shim uses:
602 /// noyalib's defaults with every knob turned to reproduce
603 /// `serde_yaml` 0.9's observable behaviour on the same input —
604 /// the behavioural half of being a drop-in replacement.
605 ///
606 /// Concretely, relative to [`ParserConfig::new`]:
607 ///
608 /// - `<<` merge keys stay ordinary entries whose alias values
609 /// resolve ([`MergeKeyPolicy::AsOrdinary`]) — serde_yaml
610 /// never implemented the merge;
611 /// - leading-zero integers (`0123`) resolve as strings and
612 /// YAML 1.1 binary literals (`0b11`) as integers — libyaml's
613 /// resolver, spec versions notwithstanding;
614 /// - a literal float overflow (`1e999`) is a string, not
615 /// infinity;
616 /// - integers past `u64::MAX` error ("JSON number out of
617 /// range" territory) instead of degrading to `f64`, and
618 /// (with the `lossless-u64` feature the shim enables)
619 /// `u64`-range integers keep full precision;
620 /// - a non-scalar mapping key is an error, not a stringified
621 /// key;
622 /// - transitive alias expansion is budgeted at 100× the
623 /// document's event count — the exact rule behind
624 /// serde_yaml's "repetition limit exceeded".
625 ///
626 /// # Examples
627 ///
628 /// ```
629 /// use noyalib::ParserConfig;
630 /// let cfg = ParserConfig::serde_yaml_compat();
631 /// assert!(cfg.leading_zero_integer_strings);
632 /// assert_eq!(cfg.alias_jump_event_factor, Some(100));
633 /// ```
634 #[must_use]
635 pub fn serde_yaml_compat() -> Self {
636 Self {
637 merge_key_policy: MergeKeyPolicy::AsOrdinary,
638 leading_zero_integer_strings: true,
639 legacy_binary_numbers: true,
640 float_overflow_strings: true,
641 integer_overflow_errors: true,
642 non_scalar_key_policy: NonScalarKeyPolicy::Error,
643 #[cfg(feature = "lossless-u64")]
644 lossless_u64_integers: true,
645 ..Self::default().with_limits(ParserLimits::profile(ParserProfile::SerdeYaml))
646 }
647 }
648
649 /// Return the resource budgets currently applied to this configuration.
650 ///
651 /// Semantic settings, policies, property interpolation, tag registries,
652 /// and include resolvers are deliberately excluded.
653 #[must_use]
654 pub fn limits(&self) -> ParserLimits {
655 ParserLimits {
656 max_depth: self.max_depth,
657 max_document_length: self.max_document_length,
658 max_stream_bytes: self.max_stream_bytes,
659 max_alias_expansions: self.max_alias_expansions,
660 max_mapping_keys: self.max_mapping_keys,
661 max_sequence_length: self.max_sequence_length,
662 max_events: self.max_events,
663 max_nodes: self.max_nodes,
664 max_total_scalar_bytes: self.max_total_scalar_bytes,
665 max_documents: self.max_documents,
666 max_merge_keys: self.max_merge_keys,
667 alias_anchor_ratio: self.alias_anchor_ratio,
668 alias_jump_event_factor: self.alias_jump_event_factor,
669 #[cfg(feature = "include")]
670 max_include_depth: self.max_include_depth,
671 #[cfg(feature = "include")]
672 max_include_sources: self.max_include_sources,
673 #[cfg(feature = "include")]
674 max_total_include_bytes: self.max_total_include_bytes,
675 }
676 }
677
678 /// Replace every resource budget while preserving semantic settings and
679 /// installed integrations.
680 ///
681 /// # Examples
682 ///
683 /// ```
684 /// use noyalib::{ParserConfig, ParserLimits, YamlVersion};
685 ///
686 /// let cfg = ParserConfig::new()
687 /// .version(YamlVersion::V1_1)
688 /// .with_limits(ParserLimits::strict());
689 /// assert_eq!(cfg.yaml_version, YamlVersion::V1_1);
690 /// assert_eq!(cfg.max_depth, 64);
691 /// ```
692 #[must_use]
693 pub fn with_limits(mut self, limits: ParserLimits) -> Self {
694 self.max_depth = limits.max_depth;
695 self.max_document_length = limits.max_document_length;
696 self.max_stream_bytes = limits.max_stream_bytes;
697 self.max_alias_expansions = limits.max_alias_expansions;
698 self.max_mapping_keys = limits.max_mapping_keys;
699 self.max_sequence_length = limits.max_sequence_length;
700 self.max_events = limits.max_events;
701 self.max_nodes = limits.max_nodes;
702 self.max_total_scalar_bytes = limits.max_total_scalar_bytes;
703 self.max_documents = limits.max_documents;
704 self.max_merge_keys = limits.max_merge_keys;
705 self.alias_anchor_ratio = limits.alias_anchor_ratio;
706 self.alias_jump_event_factor = limits.alias_jump_event_factor;
707 #[cfg(feature = "include")]
708 {
709 self.max_include_depth = limits.max_include_depth;
710 self.max_include_sources = limits.max_include_sources;
711 self.max_total_include_bytes = limits.max_total_include_bytes;
712 }
713 self
714 }
715
716 /// Install a `${KEY}` substitution table consulted after
717 /// parsing.
718 ///
719 /// Each scalar in the resulting [`Value`](crate::Value) tree is walked and
720 /// any `${name}` placeholder is replaced with the property of
721 /// that name. Pairs with [`Self::strict_properties`] to choose
722 /// between erroring or silently empty-substituting on unknown
723 /// keys, and with `${name:-default}` syntax for inline
724 /// defaults.
725 ///
726 /// # Examples
727 ///
728 /// ```
729 /// use noyalib::{from_str_with_config, ParserConfig, Value};
730 /// use std::collections::HashMap;
731 /// use std::sync::Arc;
732 ///
733 /// let mut props = HashMap::new();
734 /// props.insert("HOST".to_string(), "localhost".to_string());
735 /// let cfg = ParserConfig::new().properties(Arc::new(props));
736 /// let v: Value = from_str_with_config("url: http://${HOST}/", &cfg).unwrap();
737 /// assert_eq!(v["url"].as_str(), Some("http://localhost/"));
738 /// ```
739 #[cfg(feature = "std")]
740 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
741 #[must_use]
742 pub fn properties(
743 mut self,
744 properties: Arc<std::collections::HashMap<String, String>>,
745 ) -> Self {
746 self.properties = Some(properties);
747 self
748 }
749
750 /// Toggle strict-mode placeholder resolution.
751 ///
752 /// When `true`, an unknown `${name}` (no map entry, no
753 /// `:-default` fallback) aborts the parse. When `false`
754 /// (default), unknown placeholders are replaced with the empty
755 /// string — useful for environment-style configs where missing
756 /// variables should silently degrade.
757 ///
758 /// # Examples
759 ///
760 /// ```
761 /// use noyalib::{from_str_with_config, ParserConfig, Value};
762 /// use std::collections::HashMap;
763 /// use std::sync::Arc;
764 ///
765 /// let cfg = ParserConfig::new()
766 /// .properties(Arc::new(HashMap::new()))
767 /// .strict_properties(true);
768 /// let res: Result<Value, _> = from_str_with_config("x: ${MISSING}", &cfg);
769 /// assert!(res.is_err());
770 /// ```
771 #[cfg(feature = "std")]
772 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
773 #[must_use]
774 pub fn strict_properties(mut self, strict: bool) -> Self {
775 self.strict_properties = strict;
776 self
777 }
778
779 /// Install an `!include` directive resolver.
780 ///
781 /// Each `Value::Tagged(!include, scalar_spec)` node in the
782 /// parsed tree is replaced with the resolver's output. The
783 /// resolver is consulted with an `IncludeRequest` carrying
784 /// the verbatim spec text, a stable source-id, and the
785 /// current recursion depth.
786 ///
787 /// Pair with [`Self::max_include_depth`] to bound the
788 /// recursion ceiling. Cycle detection (A includes B includes
789 /// A) runs independently using a per-walk visited set.
790 ///
791 /// # Examples
792 ///
793 /// ```
794 /// use noyalib::include::{IncludeRequest, IncludeResolver, InputSource};
795 /// use noyalib::{ParserConfig, Result};
796 ///
797 /// let resolver = IncludeResolver::new(|req: IncludeRequest<'_>| -> Result<InputSource> {
798 /// // For an in-memory test, fabricate a YAML payload
799 /// // keyed on the spec.
800 /// Ok(InputSource::new(req.spec, format!("name: {}\n", req.spec)))
801 /// });
802 /// let cfg = ParserConfig::new().include_resolver(resolver);
803 /// # let _ = cfg;
804 /// ```
805 #[cfg(feature = "include")]
806 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
807 #[must_use]
808 pub fn include_resolver(mut self, resolver: crate::include::IncludeResolver) -> Self {
809 self.include_resolver = Some(resolver);
810 self
811 }
812
813 /// Maximum `!include` recursion depth.
814 ///
815 /// Default 24 (8 in [`Self::strict()`]). Each nested
816 /// `!include` increments the depth; once the limit is
817 /// reached, the parser aborts with
818 /// `Error::RecursionLimitExceeded`. The cap is independent
819 /// of [`Self::max_depth`] (which bounds *YAML structural*
820 /// nesting) and of the per-walk cycle-detection set (which
821 /// catches A→B→A regardless of depth).
822 #[cfg(feature = "include")]
823 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
824 #[must_use]
825 pub fn max_include_depth(mut self, depth: usize) -> Self {
826 self.max_include_depth = depth;
827 self
828 }
829
830 /// Set the maximum number of sources resolved by one include walk.
831 #[cfg(feature = "include")]
832 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
833 #[must_use]
834 pub fn max_include_sources(mut self, max: usize) -> Self {
835 self.max_include_sources = max;
836 self
837 }
838
839 /// Set the cumulative byte budget for sources returned by include resolvers.
840 #[cfg(feature = "include")]
841 #[cfg_attr(docsrs, doc(cfg(feature = "include")))]
842 #[must_use]
843 pub fn max_total_include_bytes(mut self, max: usize) -> Self {
844 self.max_total_include_bytes = max;
845 self
846 }
847
848 /// Select the YAML specification version the resolver should
849 /// honour.
850 ///
851 /// Selecting [`YamlVersion::V1_1`] is a *preset* over the three
852 /// `legacy_*` flags — equivalent to:
853 ///
854 /// ```text
855 /// cfg.legacy_booleans = true; // yes / no / on / off
856 /// cfg.legacy_octal_numbers = true; // 0644 → octal
857 /// cfg.legacy_sexagesimal = true; // 60:00 → 3600
858 /// ```
859 ///
860 /// Selecting [`YamlVersion::V1_2`] resets those three flags to
861 /// `false` so callers can revert to strict 1.2 mode without
862 /// re-creating the config from scratch. Other fields (limits,
863 /// policies, merge-key behaviour) are unaffected.
864 ///
865 /// Fine-grained overrides (e.g. "1.1 booleans but reject octal
866 /// `0644`") work as expected: call `version` first, then flip
867 /// individual flags.
868 ///
869 /// # Examples
870 ///
871 /// ```
872 /// use noyalib::{from_str_with_config, ParserConfig, Value, YamlVersion};
873 ///
874 /// let cfg = ParserConfig::new().version(YamlVersion::V1_1);
875 /// // YAML 1.1 booleans
876 /// let v: Value = from_str_with_config("on", &cfg).unwrap();
877 /// assert_eq!(v, Value::Bool(true));
878 /// // YAML 1.1 octal
879 /// let v: Value = from_str_with_config("0644", &cfg).unwrap();
880 /// assert_eq!(v, Value::from(420_i64));
881 /// // YAML 1.1 sexagesimal
882 /// let v: Value = from_str_with_config("1:30", &cfg).unwrap();
883 /// assert_eq!(v, Value::from(90_i64));
884 /// ```
885 #[must_use]
886 pub fn version(mut self, version: YamlVersion) -> Self {
887 self.yaml_version = version;
888 match version {
889 YamlVersion::V1_1 => {
890 self.legacy_booleans = true;
891 self.legacy_octal_numbers = true;
892 self.legacy_sexagesimal = true;
893 }
894 YamlVersion::V1_2 => {
895 self.legacy_booleans = false;
896 self.legacy_octal_numbers = false;
897 self.legacy_sexagesimal = false;
898 }
899 }
900 self
901 }
902
903 /// Register a [`Policy`](crate::policy::Policy) to enforce
904 /// during parsing.
905 ///
906 /// Multiple policies may be registered; they all run in
907 /// registration order, and the first error short-circuits the
908 /// parse. When any policy is present the streaming fast-path
909 /// is bypassed so the policy contract is enforced uniformly.
910 ///
911 /// # Examples
912 ///
913 /// ```
914 /// use noyalib::{from_str_with_config, ParserConfig, Value};
915 /// use noyalib::policy::DenyAnchors;
916 ///
917 /// let cfg = ParserConfig::new().with_policy(DenyAnchors);
918 /// let res: Result<Value, _> =
919 /// from_str_with_config("a: &x 1\nb: *x\n", &cfg);
920 /// assert!(res.is_err());
921 /// ```
922 #[must_use]
923 pub fn with_policy<P>(mut self, policy: P) -> Self
924 where
925 P: crate::policy::Policy + 'static,
926 {
927 self.policies.push(Arc::new(policy));
928 self
929 }
930
931 /// Set the maximum recursion depth.
932 ///
933 /// # Examples
934 ///
935 /// ```
936 /// use noyalib::ParserConfig;
937 /// let cfg = ParserConfig::new().max_depth(32);
938 /// assert_eq!(cfg.max_depth, 32);
939 /// ```
940 #[must_use]
941 pub fn max_depth(mut self, depth: usize) -> Self {
942 self.max_depth = depth;
943 self
944 }
945
946 /// Set the maximum document length.
947 ///
948 /// # Examples
949 ///
950 /// ```
951 /// use noyalib::ParserConfig;
952 /// let cfg = ParserConfig::new().max_document_length(1024);
953 /// assert_eq!(cfg.max_document_length, 1024);
954 /// ```
955 #[must_use]
956 pub fn max_document_length(mut self, len: usize) -> Self {
957 self.max_document_length = len;
958 self
959 }
960
961 /// Set the maximum alias expansions.
962 ///
963 /// # Examples
964 ///
965 /// ```
966 /// use noyalib::ParserConfig;
967 /// let cfg = ParserConfig::new().max_alias_expansions(50);
968 /// assert_eq!(cfg.max_alias_expansions, 50);
969 /// ```
970 #[must_use]
971 pub fn max_alias_expansions(mut self, expansions: usize) -> Self {
972 self.max_alias_expansions = expansions;
973 self
974 }
975
976 /// Set the maximum number of mapping keys.
977 ///
978 /// # Examples
979 ///
980 /// ```
981 /// use noyalib::ParserConfig;
982 /// let cfg = ParserConfig::new().max_mapping_keys(100);
983 /// assert_eq!(cfg.max_mapping_keys, 100);
984 /// ```
985 #[must_use]
986 pub fn max_mapping_keys(mut self, max: usize) -> Self {
987 self.max_mapping_keys = max;
988 self
989 }
990
991 /// Set the maximum sequence length.
992 ///
993 /// # Examples
994 ///
995 /// ```
996 /// use noyalib::ParserConfig;
997 /// let cfg = ParserConfig::new().max_sequence_length(100);
998 /// assert_eq!(cfg.max_sequence_length, 100);
999 /// ```
1000 #[must_use]
1001 pub fn max_sequence_length(mut self, max: usize) -> Self {
1002 self.max_sequence_length = max;
1003 self
1004 }
1005
1006 /// Set the maximum total parser-event budget.
1007 ///
1008 /// # Examples
1009 ///
1010 /// ```
1011 /// use noyalib::ParserConfig;
1012 /// let cfg = ParserConfig::new().max_events(50_000);
1013 /// assert_eq!(cfg.max_events, 50_000);
1014 /// ```
1015 #[must_use]
1016 pub fn max_events(mut self, max: usize) -> Self {
1017 self.max_events = max;
1018 self
1019 }
1020
1021 /// Set the maximum total `Value` node budget.
1022 ///
1023 /// # Examples
1024 ///
1025 /// ```
1026 /// use noyalib::ParserConfig;
1027 /// let cfg = ParserConfig::new().max_nodes(10_000);
1028 /// assert_eq!(cfg.max_nodes, 10_000);
1029 /// ```
1030 #[must_use]
1031 pub fn max_nodes(mut self, max: usize) -> Self {
1032 self.max_nodes = max;
1033 self
1034 }
1035
1036 /// Set the maximum cumulative scalar-byte budget.
1037 ///
1038 /// Distinct from [`Self::max_document_length`] — this caps
1039 /// scalar bytes after alias expansion.
1040 ///
1041 /// # Examples
1042 ///
1043 /// ```
1044 /// use noyalib::ParserConfig;
1045 /// let cfg = ParserConfig::new().max_total_scalar_bytes(8 * 1024 * 1024);
1046 /// assert_eq!(cfg.max_total_scalar_bytes, 8 * 1024 * 1024);
1047 /// ```
1048 #[must_use]
1049 pub fn max_total_scalar_bytes(mut self, max: usize) -> Self {
1050 self.max_total_scalar_bytes = max;
1051 self
1052 }
1053
1054 /// Set the maximum document count for multi-document streams.
1055 ///
1056 /// # Examples
1057 ///
1058 /// ```
1059 /// use noyalib::ParserConfig;
1060 /// let cfg = ParserConfig::new().max_documents(64);
1061 /// assert_eq!(cfg.max_documents, 64);
1062 /// ```
1063 #[must_use]
1064 pub fn max_documents(mut self, max: usize) -> Self {
1065 self.max_documents = max;
1066 self
1067 }
1068
1069 /// Set the maximum total byte length of a multi-document stream.
1070 ///
1071 /// # Examples
1072 ///
1073 /// ```
1074 /// use noyalib::ParserConfig;
1075 /// let cfg = ParserConfig::new().max_stream_bytes(8 * 1024 * 1024);
1076 /// assert_eq!(cfg.max_stream_bytes, 8 * 1024 * 1024);
1077 /// ```
1078 #[must_use]
1079 pub fn max_stream_bytes(mut self, max: usize) -> Self {
1080 self.max_stream_bytes = max;
1081 self
1082 }
1083
1084 /// Set the maximum merge-key count budget.
1085 ///
1086 /// # Examples
1087 ///
1088 /// ```
1089 /// use noyalib::ParserConfig;
1090 /// let cfg = ParserConfig::new().max_merge_keys(1_000);
1091 /// assert_eq!(cfg.max_merge_keys, 1_000);
1092 /// ```
1093 #[must_use]
1094 pub fn max_merge_keys(mut self, max: usize) -> Self {
1095 self.max_merge_keys = max;
1096 self
1097 }
1098
1099 /// Set the indentation-validation mode.
1100 ///
1101 /// # Examples
1102 ///
1103 /// ```
1104 /// use noyalib::{ParserConfig, RequireIndent};
1105 /// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
1106 /// assert_eq!(cfg.require_indent, RequireIndent::Even);
1107 /// ```
1108 #[must_use]
1109 pub fn require_indent(mut self, mode: RequireIndent) -> Self {
1110 self.require_indent = mode;
1111 self
1112 }
1113
1114 /// Set the alias-to-anchor ratio heuristic.
1115 ///
1116 /// Pass `Some(ratio)` to enable the billion-laughs guard,
1117 /// `None` to disable.
1118 ///
1119 /// # Examples
1120 ///
1121 /// ```
1122 /// use noyalib::ParserConfig;
1123 /// let cfg = ParserConfig::new().alias_anchor_ratio(Some(20.0));
1124 /// assert_eq!(cfg.alias_anchor_ratio, Some(20.0));
1125 /// ```
1126 #[must_use]
1127 pub fn alias_anchor_ratio(mut self, ratio: Option<f64>) -> Self {
1128 self.alias_anchor_ratio = ratio;
1129 self
1130 }
1131
1132 /// Set the duplicate key policy.
1133 ///
1134 /// # Examples
1135 ///
1136 /// ```
1137 /// use noyalib::{DuplicateKeyPolicy, ParserConfig};
1138 /// let cfg = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
1139 /// assert_eq!(cfg.duplicate_key_policy, DuplicateKeyPolicy::Error);
1140 /// ```
1141 #[must_use]
1142 pub fn duplicate_key_policy(mut self, policy: DuplicateKeyPolicy) -> Self {
1143 self.duplicate_key_policy = policy;
1144 self
1145 }
1146
1147 /// Enable or disable strict booleans.
1148 ///
1149 /// # Examples
1150 ///
1151 /// ```
1152 /// use noyalib::ParserConfig;
1153 /// let cfg = ParserConfig::new().strict_booleans(true);
1154 /// assert!(cfg.strict_booleans);
1155 /// ```
1156 #[must_use]
1157 pub fn strict_booleans(mut self, strict: bool) -> Self {
1158 self.strict_booleans = strict;
1159 self
1160 }
1161
1162 /// Enable or disable legacy booleans.
1163 ///
1164 /// # Examples
1165 ///
1166 /// ```
1167 /// use noyalib::ParserConfig;
1168 /// let cfg = ParserConfig::new().legacy_booleans(true);
1169 /// assert!(cfg.legacy_booleans);
1170 /// ```
1171 #[must_use]
1172 pub fn legacy_booleans(mut self, legacy: bool) -> Self {
1173 self.legacy_booleans = legacy;
1174 self
1175 }
1176
1177 /// Attach a [`TagRegistry`](crate::TagRegistry) so the streaming
1178 /// deserializer strips listed custom tags instead of routing them
1179 /// through the AST.
1180 ///
1181 /// See the [`tag_registry`](crate::tag_registry) module
1182 /// documentation for when to use this versus `#[serde(rename)]`.
1183 ///
1184 /// # Examples
1185 ///
1186 /// ```
1187 /// use noyalib::{ParserConfig, TagRegistry};
1188 /// use std::sync::Arc;
1189 /// let reg = Arc::new(TagRegistry::new().with("!Celsius"));
1190 /// let cfg = ParserConfig::new().tag_registry(Arc::clone(®));
1191 /// assert!(cfg.tag_registry.is_some());
1192 /// ```
1193 #[must_use]
1194 pub fn tag_registry(mut self, registry: Arc<crate::TagRegistry>) -> Self {
1195 self.tag_registry = Some(registry);
1196 self
1197 }
1198
1199 /// Set the policy for handling the YAML merge key (`<<`).
1200 ///
1201 /// # Examples
1202 ///
1203 /// ```
1204 /// use noyalib::{MergeKeyPolicy, ParserConfig};
1205 /// let cfg = ParserConfig::new().merge_key_policy(MergeKeyPolicy::AsOrdinary);
1206 /// assert_eq!(cfg.merge_key_policy, MergeKeyPolicy::AsOrdinary);
1207 /// ```
1208 #[must_use]
1209 pub fn merge_key_policy(mut self, policy: MergeKeyPolicy) -> Self {
1210 self.merge_key_policy = policy;
1211 self
1212 }
1213
1214 /// Toggle schema-free plain-scalar resolution. When `true`,
1215 /// every plain scalar becomes a string regardless of whether
1216 /// it would normally resolve to `null`, `bool`, integer, or
1217 /// float.
1218 ///
1219 /// # Examples
1220 ///
1221 /// ```
1222 /// use noyalib::ParserConfig;
1223 /// let cfg = ParserConfig::new().no_schema(true);
1224 /// assert!(cfg.no_schema);
1225 /// ```
1226 #[must_use]
1227 pub fn no_schema(mut self, no_schema: bool) -> Self {
1228 self.no_schema = no_schema;
1229 self
1230 }
1231
1232 /// Toggle YAML 1.1-style bare `0`-prefix octal parsing
1233 /// (e.g. `0644` → 420). Off by default; YAML 1.2 requires the
1234 /// `0o` prefix.
1235 ///
1236 /// # Examples
1237 ///
1238 /// ```
1239 /// use noyalib::ParserConfig;
1240 /// let cfg = ParserConfig::new().legacy_octal_numbers(true);
1241 /// assert!(cfg.legacy_octal_numbers);
1242 /// ```
1243 #[must_use]
1244 pub fn legacy_octal_numbers(mut self, on: bool) -> Self {
1245 self.legacy_octal_numbers = on;
1246 self
1247 }
1248
1249 /// Toggle the migration-helper behaviour where
1250 /// `!!binary "ABCD"` deserializes into a [`String`] target as
1251 /// the literal base64 source string. The canonical bytes
1252 /// path (`Vec<u8>`, `serde_bytes::ByteBuf`) is unaffected —
1253 /// it always decodes the base64 payload.
1254 ///
1255 /// # Examples
1256 ///
1257 /// ```
1258 /// use noyalib::ParserConfig;
1259 /// let cfg = ParserConfig::new().ignore_binary_tag_for_string(true);
1260 /// assert!(cfg.ignore_binary_tag_for_string);
1261 /// ```
1262 #[must_use]
1263 pub fn ignore_binary_tag_for_string(mut self, on: bool) -> Self {
1264 self.ignore_binary_tag_for_string = on;
1265 self
1266 }
1267
1268 /// When enabled, a plain scalar deserializes into a [`String`]
1269 /// (or `char`) target as its source text even where the YAML 1.2
1270 /// schema resolves it to a number, boolean, or null:
1271 /// `password: 123456` gives `"123456"`, `~` gives `"~"`, an empty
1272 /// value gives `""`. Off by default: a `String` field refuses a
1273 /// non-string plain scalar. Quoted scalars are strings either
1274 /// way. Matches what serde_yaml did for typed targets.
1275 ///
1276 /// # Examples
1277 ///
1278 /// ```
1279 /// use noyalib::ParserConfig;
1280 /// let cfg = ParserConfig::new().plain_scalar_strings(true);
1281 /// assert!(cfg.plain_scalar_strings);
1282 /// ```
1283 #[must_use]
1284 pub fn plain_scalar_strings(mut self, on: bool) -> Self {
1285 self.plain_scalar_strings = on;
1286 self
1287 }
1288
1289 /// Toggle YAML 1.1-style sexagesimal number parsing
1290 /// (`60:00` → 3 600). Off by default; YAML 1.2 dropped the
1291 /// sexagesimal schema, so plain `1:30:00` would otherwise
1292 /// surface as a string.
1293 ///
1294 /// # Examples
1295 ///
1296 /// ```
1297 /// use noyalib::ParserConfig;
1298 /// let cfg = ParserConfig::new().legacy_sexagesimal(true);
1299 /// assert!(cfg.legacy_sexagesimal);
1300 /// ```
1301 #[must_use]
1302 pub fn legacy_sexagesimal(mut self, on: bool) -> Self {
1303 self.legacy_sexagesimal = on;
1304 self
1305 }
1306
1307 /// Enable or disable lossless unsigned integer resolution.
1308 ///
1309 /// With the `lossless-u64` feature enabled, setting this to
1310 /// `true` lets YAML integer scalars in `(i64::MAX, u64::MAX]`
1311 /// resolve as `Number::Unsigned` instead of falling through to
1312 /// `Number::Float`.
1313 #[cfg(feature = "lossless-u64")]
1314 #[cfg_attr(docsrs, doc(cfg(feature = "lossless-u64")))]
1315 #[must_use]
1316 pub fn lossless_u64_integers(mut self, on: bool) -> Self {
1317 self.lossless_u64_integers = on;
1318 self
1319 }
1320}
1321
1322/// Policy for handling the YAML merge key (`<<`) during parsing.
1323///
1324/// YAML 1.2 §10.2 defines `<<` as a "merge key" that, when used as
1325/// a mapping key, splices the value's mapping (or sequence of
1326/// mappings) into the enclosing mapping. The variants below let
1327/// callers opt out of that behaviour.
1328///
1329/// # Examples
1330///
1331/// Indentation-validation mode for the YAML scanner.
1332///
1333/// Issue #6 surface — every block-context indent transition is
1334/// classified per the chosen mode. The default
1335/// ([`RequireIndent::Unchecked`]) accepts any well-formed YAML
1336/// indent (the YAML 1.2 spec mandate), which is what every
1337/// other Rust YAML parser does.
1338///
1339/// Stricter modes are useful in pipelines that require uniform
1340/// indentation house style (linters, formatters, reviewer
1341/// gates) — a config file with mixed `2`-space and `4`-space
1342/// indent passes by-spec but fails consistency review.
1343///
1344/// # Variants
1345///
1346/// - [`RequireIndent::Unchecked`] (default) — by-spec mode.
1347/// - [`RequireIndent::Even`] — every indent delta must be even.
1348/// - `RequireIndent::Divisible(N)` — every indent delta must
1349/// be divisible by `N`.
1350/// - `RequireIndent::Uniform(Some(N))` — every indent delta
1351/// must equal `N`. `None` means "auto-detect from the first
1352/// delta and require the rest to match it".
1353///
1354/// # Examples
1355///
1356/// ```
1357/// use noyalib::{ParserConfig, RequireIndent};
1358/// let cfg = ParserConfig::new().require_indent(RequireIndent::Even);
1359/// assert_eq!(cfg.require_indent, RequireIndent::Even);
1360/// ```
1361#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1362#[non_exhaustive]
1363pub enum RequireIndent {
1364 /// Accept any indent transition the YAML 1.2 spec allows.
1365 /// Default.
1366 #[default]
1367 Unchecked,
1368 /// Indent delta must be even (`2`, `4`, `6`, …). The most
1369 /// common house-style.
1370 Even,
1371 /// Indent delta must be divisible by `N`.
1372 Divisible(usize),
1373 /// `Some(N)`: every indent delta must equal `N`.
1374 /// `None`: the first delta sets the standard for the
1375 /// document; subsequent deltas must match it.
1376 Uniform(Option<usize>),
1377}
1378
1379/// ```
1380/// use noyalib::MergeKeyPolicy;
1381/// assert_eq!(MergeKeyPolicy::default(), MergeKeyPolicy::Auto);
1382/// ```
1383#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1384#[non_exhaustive]
1385pub enum MergeKeyPolicy {
1386 /// Apply the YAML 1.2 merge-key semantics — `<<:` keys trigger
1387 /// automatic merge of the value into the enclosing mapping.
1388 /// Default.
1389 ///
1390 /// Only a **plain** `<<` scalar is a merge key. The YAML merge
1391 /// type gives `tag:yaml.org,2002:merge` to a plain `<<`; a quoted
1392 /// `"<<"` and an alias that happens to resolve to the string
1393 /// `<<` both resolve to `tag:yaml.org,2002:str` and stay ordinary
1394 /// keys, whatever this policy is set to.
1395 #[default]
1396 Auto,
1397 /// Treat `<<` as an ordinary string key. The mapping retains a
1398 /// literal `<<` entry whose value is whatever the YAML
1399 /// document supplied. Useful when round-tripping configuration
1400 /// files that happen to contain a `<<` key for non-merge
1401 /// reasons.
1402 AsOrdinary,
1403 /// Reject any document that contains a `<<` key with
1404 /// [`crate::Error::Custom`]. Useful for schema-strict pipelines
1405 /// where merge keys are forbidden.
1406 Error,
1407}
1408
1409/// Policy for handling a non-scalar mapping key (`[a, b]: v`,
1410/// `{k: v}: w`).
1411///
1412/// # Examples
1413///
1414/// ```
1415/// use noyalib::NonScalarKeyPolicy;
1416/// assert_eq!(NonScalarKeyPolicy::default(), NonScalarKeyPolicy::Stringify);
1417/// ```
1418#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1419#[non_exhaustive]
1420pub enum NonScalarKeyPolicy {
1421 /// Convert the key to its deterministic string form
1422 /// (`"[a, b]"`), reporting a `KeyCollision` when two distinct
1423 /// keys stringify identically. Default.
1424 #[default]
1425 Stringify,
1426 /// Abort the parse with
1427 /// [`ErrorKind::NonScalarKey`](crate::ErrorKind) — the
1428 /// behaviour `serde_yaml` 0.9 had when the target was a
1429 /// string-keyed structure ("invalid type: sequence, expected a
1430 /// string key").
1431 Error,
1432}
1433
1434/// Policy for handling duplicate keys in a YAML mapping.
1435///
1436/// # Examples
1437///
1438/// ```
1439/// use noyalib::DuplicateKeyPolicy;
1440/// assert_eq!(DuplicateKeyPolicy::default(), DuplicateKeyPolicy::Last);
1441/// ```
1442#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1443#[non_exhaustive]
1444pub enum DuplicateKeyPolicy {
1445 /// Use the first occurrence of the key; ignore subsequent ones.
1446 First,
1447 /// Use the last occurrence of the key (YAML 1.2 default).
1448 #[default]
1449 Last,
1450 /// Return an error if a duplicate key is encountered.
1451 Error,
1452}