Skip to main content

noyalib/
value.rs

1//! YAML value types.
2
3// SPDX-License-Identifier: MIT OR Apache-2.0
4// Copyright (c) 2026 Noyalib. All rights reserved.
5
6use crate::prelude::*;
7use core::cmp::Ordering;
8use core::hash::{Hash, Hasher};
9
10mod convert;
11mod mapping;
12mod number;
13mod serde_impl;
14pub use mapping::{Mapping, MappingAny};
15pub(crate) use number::write_float;
16pub use number::{Number, ParseNumberError};
17
18/// A YAML sequence (array/list).
19pub type Sequence = Vec<Value>;
20
21#[cfg(feature = "arbitrary")]
22#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
23mod arbitrary_impls;
24mod tag;
25pub use tag::{MaybeTag, Tag, TaggedValue, check_for_tag, nobang};
26
27/// Represents any valid YAML value.
28#[derive(Debug, Clone, Default)]
29pub enum Value {
30    /// Represents a YAML null value.
31    #[default]
32    Null,
33    /// Represents a YAML boolean.
34    Bool(bool),
35    /// Represents a YAML number (integer or float).
36    Number(Number),
37    /// Represents a YAML string.
38    String(String),
39    /// Represents a YAML sequence (array).
40    Sequence(Sequence),
41    /// Represents a YAML mapping (object).
42    Mapping(Mapping),
43    /// Represents a tagged YAML value.
44    Tagged(Box<TaggedValue>),
45}
46
47impl Value {
48    /// Returns `true` if the value is `Value::Null`.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use noyalib::Value;
54    /// assert!(Value::Null.is_null());
55    /// assert!(!Value::from(false).is_null());
56    /// ```
57    #[must_use]
58    pub fn is_null(&self) -> bool {
59        matches!(self, Self::Null)
60    }
61
62    /// Returns `true` if the value is a boolean.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use noyalib::Value;
68    /// assert!(Value::from(true).is_bool());
69    /// assert!(!Value::from(1_i64).is_bool());
70    /// ```
71    #[must_use]
72    pub fn is_bool(&self) -> bool {
73        matches!(self, Self::Bool(_))
74    }
75
76    /// Returns `true` if the value is a number (integer or float).
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use noyalib::Value;
82    /// assert!(Value::from(42_i64).is_number());
83    /// assert!(Value::from(1.5).is_number());
84    /// assert!(!Value::from("42").is_number());
85    /// ```
86    #[must_use]
87    pub fn is_number(&self) -> bool {
88        matches!(self, Self::Number(_))
89    }
90
91    /// Returns `true` if the value is a string.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use noyalib::Value;
97    /// assert!(Value::from("hello").is_string());
98    /// assert!(!Value::from(42_i64).is_string());
99    /// ```
100    #[must_use]
101    pub fn is_string(&self) -> bool {
102        matches!(self, Self::String(_))
103    }
104
105    /// Returns `true` if the value is a sequence (YAML list).
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use noyalib::{from_str, Value};
111    /// let v: Value = from_str("[1, 2, 3]").unwrap();
112    /// assert!(v.is_sequence());
113    /// ```
114    #[must_use]
115    pub fn is_sequence(&self) -> bool {
116        matches!(self, Self::Sequence(_))
117    }
118
119    /// Returns `true` if the value is a mapping (YAML map).
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use noyalib::{from_str, Value};
125    /// let v: Value = from_str("a: 1\nb: 2\n").unwrap();
126    /// assert!(v.is_mapping());
127    /// ```
128    #[must_use]
129    pub fn is_mapping(&self) -> bool {
130        matches!(self, Self::Mapping(_))
131    }
132
133    /// Returns `true` if the value is tagged (custom YAML tag).
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use noyalib::{from_str, Value};
139    /// let v: Value = from_str("!Custom 'hello'\n").unwrap();
140    /// assert!(v.is_tagged());
141    /// ```
142    #[must_use]
143    pub fn is_tagged(&self) -> bool {
144        matches!(self, Self::Tagged(_))
145    }
146
147    /// Returns the value as a boolean if it is one.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use noyalib::Value;
153    /// assert_eq!(Value::from(true).as_bool(), Some(true));
154    /// assert_eq!(Value::from("true").as_bool(), None);
155    /// ```
156    #[must_use]
157    pub fn as_bool(&self) -> Option<bool> {
158        match self {
159            Self::Bool(b) => Some(*b),
160            _ => None,
161        }
162    }
163
164    /// Returns `Some(())` if the value is null, `None` otherwise.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use noyalib::Value;
170    /// assert_eq!(Value::Null.as_null(), Some(()));
171    /// assert_eq!(Value::from(0_i64).as_null(), None);
172    /// ```
173    #[must_use]
174    pub fn as_null(&self) -> Option<()> {
175        match self {
176            Self::Null => Some(()),
177            _ => None,
178        }
179    }
180
181    /// Returns the value as an `i64` if it is an integer.
182    ///
183    /// Floats return `None` even when the underlying value is a
184    /// whole number; the type tag is part of the test.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use noyalib::Value;
190    /// assert_eq!(Value::from(42_i64).as_i64(), Some(42));
191    /// assert_eq!(Value::from(1.5).as_i64(), None);
192    /// ```
193    #[must_use]
194    pub fn as_i64(&self) -> Option<i64> {
195        match self {
196            Self::Number(n) => n.as_i64(),
197            _ => None,
198        }
199    }
200
201    /// Returns the value as a `u64` if it is a non-negative integer.
202    ///
203    /// Negative integers return `None`. Floats also return `None`.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use noyalib::Value;
209    /// assert_eq!(Value::from(42_i64).as_u64(), Some(42));
210    /// assert_eq!(Value::from(-1_i64).as_u64(), None);
211    /// ```
212    #[must_use]
213    pub fn as_u64(&self) -> Option<u64> {
214        match self {
215            Self::Number(n) => n.as_u64(),
216            _ => None,
217        }
218    }
219
220    /// Returns the value as an `f64` if it is a number.
221    ///
222    /// Integers are widened to `f64` (with the usual `i64 → f64`
223    /// precision loss for magnitudes above 2^53).
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use noyalib::Value;
229    /// assert_eq!(Value::from(42_i64).as_f64(), Some(42.0));
230    /// assert_eq!(Value::from(1.5).as_f64(), Some(1.5));
231    /// assert_eq!(Value::from("42").as_f64(), None);
232    /// ```
233    #[must_use]
234    pub fn as_f64(&self) -> Option<f64> {
235        match self {
236            Self::Number(n) => Some(n.as_f64()),
237            _ => None,
238        }
239    }
240
241    /// Returns `true` if the value is an integer that fits in `i64`.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use noyalib::Value;
247    /// assert!(Value::from(42_i64).is_i64());
248    /// assert!(!Value::from(1.5).is_i64());
249    /// ```
250    #[must_use]
251    pub fn is_i64(&self) -> bool {
252        match self {
253            Self::Number(n) => n.is_i64(),
254            _ => false,
255        }
256    }
257
258    /// Returns `true` if the value is a non-negative integer that fits in `u64`.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use noyalib::Value;
264    /// assert!(Value::from(42_i64).is_u64());
265    /// assert!(!Value::from(-1_i64).is_u64());
266    /// ```
267    #[must_use]
268    pub fn is_u64(&self) -> bool {
269        match self {
270            Self::Number(n) => n.is_u64(),
271            _ => false,
272        }
273    }
274
275    /// Returns `true` if the value is a number (always convertible to `f64`).
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use noyalib::Value;
281    /// assert!(Value::from(42_i64).is_f64());
282    /// assert!(Value::from(1.5).is_f64());
283    /// ```
284    #[must_use]
285    pub fn is_f64(&self) -> bool {
286        matches!(self, Self::Number(_))
287    }
288
289    /// Returns the value as a string slice if it is a string.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use noyalib::Value;
295    /// assert_eq!(Value::from("hello").as_str(), Some("hello"));
296    /// assert_eq!(Value::from(42_i64).as_str(), None);
297    /// ```
298    #[must_use]
299    pub fn as_str(&self) -> Option<&str> {
300        match self {
301            Self::String(s) => Some(s),
302            _ => None,
303        }
304    }
305
306    /// Returns the value as a sequence if it is one.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use noyalib::{from_str, Value};
312    /// let v: Value = from_str("[1, 2, 3]").unwrap();
313    /// assert_eq!(v.as_sequence().map(|s| s.len()), Some(3));
314    /// ```
315    #[must_use]
316    pub fn as_sequence(&self) -> Option<&Sequence> {
317        match self {
318            Self::Sequence(s) => Some(s),
319            _ => None,
320        }
321    }
322
323    /// Returns the value as a mutable sequence if it is one.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use noyalib::{from_str, Value};
329    /// let mut v: Value = from_str("[1, 2]").unwrap();
330    /// if let Some(seq) = v.as_sequence_mut() {
331    ///     seq.push(Value::from(3_i64));
332    /// }
333    /// assert_eq!(v.as_sequence().unwrap().len(), 3);
334    /// ```
335    #[must_use]
336    pub fn as_sequence_mut(&mut self) -> Option<&mut Sequence> {
337        match self {
338            Self::Sequence(s) => Some(s),
339            _ => None,
340        }
341    }
342
343    /// Returns the value as a mapping if it is one.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use noyalib::{from_str, Value};
349    /// let v: Value = from_str("a: 1\nb: 2\n").unwrap();
350    /// let m = v.as_mapping().unwrap();
351    /// assert_eq!(m.get("a").and_then(Value::as_i64), Some(1));
352    /// ```
353    #[must_use]
354    pub fn as_mapping(&self) -> Option<&Mapping> {
355        match self {
356            Self::Mapping(m) => Some(m),
357            _ => None,
358        }
359    }
360
361    /// Returns the value as a mutable mapping if it is one.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use noyalib::{from_str, Value};
367    /// let mut v: Value = from_str("a: 1\n").unwrap();
368    /// if let Some(m) = v.as_mapping_mut() {
369    ///     m.insert("b", Value::from(2_i64));
370    /// }
371    /// assert_eq!(v.as_mapping().unwrap().len(), 2);
372    /// ```
373    #[must_use]
374    pub fn as_mapping_mut(&mut self) -> Option<&mut Mapping> {
375        match self {
376            Self::Mapping(m) => Some(m),
377            _ => None,
378        }
379    }
380
381    /// Returns the value as a tagged value if it is one.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use noyalib::{from_str, Value};
387    /// let v: Value = from_str("!Custom 'hi'\n").unwrap();
388    /// let tv = v.as_tagged().unwrap();
389    /// assert_eq!(tv.tag().as_str(), "!Custom");
390    /// ```
391    #[must_use]
392    pub fn as_tagged(&self) -> Option<&TaggedValue> {
393        match self {
394            Self::Tagged(t) => Some(t),
395            _ => None,
396        }
397    }
398
399    /// Returns the value as a mutable tagged value if it is one.
400    ///
401    /// # Examples
402    ///
403    /// ```
404    /// use noyalib::{from_str, Tag, Value};
405    /// let mut v: Value = from_str("!A 'x'\n").unwrap();
406    /// if let Some(tv) = v.as_tagged_mut() {
407    ///     // mutate inner via value_mut
408    ///     *tv.value_mut() = Value::from("y");
409    /// }
410    /// assert_eq!(v.as_tagged().unwrap().value().as_str(), Some("y"));
411    /// # let _ = Tag::new("!A");
412    /// ```
413    #[must_use]
414    pub fn as_tagged_mut(&mut self) -> Option<&mut TaggedValue> {
415        match self {
416            Self::Tagged(t) => Some(t),
417            _ => None,
418        }
419    }
420
421    /// Index into a sequence or mapping.
422    ///
423    /// Accepts a string key (for mappings) or a `usize` index
424    /// (for sequences).
425    ///
426    /// # Examples
427    ///
428    /// ```
429    /// use noyalib::{from_str, Value};
430    /// let v: Value = from_str("a: [1, 2, 3]\n").unwrap();
431    /// assert_eq!(v.get("a").unwrap().get(0).and_then(Value::as_i64), Some(1));
432    /// ```
433    #[must_use]
434    pub fn get<I: ValueIndex>(&self, index: I) -> Option<&Self> {
435        index.index_into(self)
436    }
437
438    /// Mutably index into a sequence or mapping.
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use noyalib::{from_str, Value};
444    /// let mut v: Value = from_str("a: 1\n").unwrap();
445    /// if let Some(slot) = v.get_mut("a") {
446    ///     *slot = Value::from(2_i64);
447    /// }
448    /// assert_eq!(v.get("a").and_then(Value::as_i64), Some(2));
449    /// ```
450    #[must_use]
451    pub fn get_mut<I: ValueIndex>(&mut self, index: I) -> Option<&mut Self> {
452        index.index_into_mut(self)
453    }
454
455    /// Access a nested value through a prevalidated [`QueryPath`].
456    ///
457    /// Unlike [`get_path`](Self::get_path), this separates path syntax
458    /// validation from lookup, so a missing value cannot be confused with
459    /// malformed user input.
460    #[must_use]
461    pub fn get_query_path(&self, path: &QueryPath) -> Option<&Self> {
462        let mut current = self;
463
464        for segment in path.segments() {
465            current = match segment {
466                QuerySegment::Key(key) => current.get(key.as_str())?,
467                QuerySegment::Index(idx) => current.get(*idx)?,
468                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => {
469                    return self.query_path(path).into_iter().next();
470                }
471            };
472        }
473
474        Some(current)
475    }
476
477    /// Access a nested value using a path string.
478    ///
479    /// Supports dot notation for mappings and bracket notation for sequences:
480    /// - `"foo.bar"` - access key "bar" in mapping "foo"
481    /// - `"items[0]"` - access index 0 in sequence "items"
482    /// - `"items[0].name"` - access key "name" in first element of sequence
483    ///   "items"
484    /// - `"labels[\"app.kubernetes.io/name\"]"` - a bracket-quoted segment
485    ///   is one key, whatever it contains; see [`crate::path`] for the
486    ///   grammar and for [`crate::path::join_keys`], which spells a path
487    ///   from literal keys
488    ///
489    /// # Examples
490    ///
491    /// ```rust
492    /// use noyalib::{from_str, Value};
493    ///
494    /// let yaml = r#"
495    /// server:
496    ///   host: localhost
497    ///   port: 8080
498    /// items:
499    ///   - name: first
500    ///   - name: second
501    /// "#;
502    ///
503    /// let value: Value = from_str(yaml).unwrap();
504    ///
505    /// assert_eq!(
506    ///     value.get_path("server.host").unwrap().as_str(),
507    ///     Some("localhost")
508    /// );
509    /// assert_eq!(value.get_path("server.port").unwrap().as_i64(), Some(8080));
510    /// assert_eq!(
511    ///     value.get_path("items[0].name").unwrap().as_str(),
512    ///     Some("first")
513    /// );
514    /// assert_eq!(
515    ///     value.get_path("items[1].name").unwrap().as_str(),
516    ///     Some("second")
517    /// );
518    /// ```
519    #[must_use]
520    pub fn get_path(&self, path: &str) -> Option<&Self> {
521        let segments = parse_path(path);
522        let mut current = self;
523
524        for segment in segments {
525            current = match segment {
526                QuerySegment::Key(key) => current.get(key.as_str())?,
527                QuerySegment::Index(idx) => current.get(idx)?,
528                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => {
529                    // For get_path, return the first match
530                    return self.query(path).into_iter().next();
531                }
532            };
533        }
534
535        Some(current)
536    }
537
538    /// Query nested values through a prevalidated [`QueryPath`].
539    #[must_use]
540    pub fn query_path(&self, path: &QueryPath) -> Vec<&Self> {
541        let mut results = Vec::new();
542        query_recursive(self, path.segments(), 0, &mut results);
543        results
544    }
545
546    /// Query nested values using an extended path expression.
547    ///
548    /// Returns all matching values. Supports:
549    /// - Dot notation: `"foo.bar.baz"`
550    /// - Bracket indexing: `"items[0]"`
551    /// - Quoted keys: `"labels[\"app.kubernetes.io/name\"]"` — one key,
552    ///   whatever it contains (see [`crate::path`])
553    /// - Wildcard: `"items[*]"` or `"items.*"` — matches all children
554    /// - Recursive descent: `"..name"` — finds `name` at any depth
555    ///
556    /// # Examples
557    ///
558    /// ```rust
559    /// use noyalib::{from_str, Value};
560    ///
561    /// let yaml = "items:\n  - name: a\n    v: 1\n  - name: b\n    v: 2\n";
562    /// let value: Value = from_str(yaml).unwrap();
563    ///
564    /// // Wildcard: all items
565    /// let all = value.query("items[*].name");
566    /// assert_eq!(all.len(), 2);
567    ///
568    /// // Recursive descent: find "name" at any depth
569    /// let names = value.query("..name");
570    /// assert_eq!(names.len(), 2);
571    /// ```
572    #[must_use]
573    pub fn query(&self, path: &str) -> Vec<&Self> {
574        let segments = parse_path(path);
575        let mut results = Vec::new();
576        query_recursive(self, &segments, 0, &mut results);
577        results
578    }
579
580    /// Mutably access a nested value through a prevalidated [`QueryPath`].
581    ///
582    /// Wildcard and recursive-descent segments do not identify one mutable
583    /// value and therefore return `None`.
584    #[must_use]
585    pub fn get_query_path_mut(&mut self, path: &QueryPath) -> Option<&mut Self> {
586        let mut current = self;
587
588        for segment in path.segments() {
589            current = match segment {
590                QuerySegment::Key(key) => current.get_mut(key.as_str())?,
591                QuerySegment::Index(idx) => current.get_mut(*idx)?,
592                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => return None,
593            };
594        }
595
596        Some(current)
597    }
598
599    /// Mutably access a nested value using a path string.
600    ///
601    /// See [`get_path`](Self::get_path) for path syntax documentation.
602    ///
603    /// # Examples
604    ///
605    /// ```rust
606    /// use noyalib::{from_str, Value};
607    ///
608    /// let yaml = "server:\n  port: 8080\n";
609    /// let mut value: Value = from_str(yaml).unwrap();
610    ///
611    /// if let Some(port) = value.get_path_mut("server.port") {
612    ///     *port = Value::from(9090);
613    /// }
614    ///
615    /// assert_eq!(value.get_path("server.port").unwrap().as_i64(), Some(9090));
616    /// ```
617    #[must_use]
618    pub fn get_path_mut(&mut self, path: &str) -> Option<&mut Self> {
619        let segments = parse_path(path);
620        let mut current = self;
621
622        for segment in segments {
623            current = match segment {
624                QuerySegment::Key(key) => current.get_mut(key.as_str())?,
625                QuerySegment::Index(idx) => current.get_mut(idx)?,
626                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => return None,
627            };
628        }
629
630        Some(current)
631    }
632
633    /// Deep merge another value into this one.
634    ///
635    /// Merge behavior:
636    /// - Mappings: keys from `other` are merged recursively; `other` keys
637    ///   override `self` keys
638    /// - Sequences: `other` sequence replaces `self` sequence (use
639    ///   `merge_concat` for concatenation)
640    /// - Scalars: `other` value replaces `self` value
641    /// - Null in `other`: replaces `self` value
642    ///
643    /// # Examples
644    ///
645    /// ```rust
646    /// use noyalib::{from_str, Value};
647    ///
648    /// let mut base: Value = from_str(
649    ///     "
650    /// server:
651    ///   host: localhost
652    ///   port: 8080
653    /// ",
654    /// )
655    /// .unwrap();
656    ///
657    /// let override_val: Value = from_str(
658    ///     "
659    /// server:
660    ///   port: 9090
661    ///   ssl: true
662    /// ",
663    /// )
664    /// .unwrap();
665    ///
666    /// base.merge(override_val);
667    ///
668    /// assert_eq!(
669    ///     base.get_path("server.host").unwrap().as_str(),
670    ///     Some("localhost")
671    /// );
672    /// assert_eq!(base.get_path("server.port").unwrap().as_i64(), Some(9090));
673    /// assert_eq!(base.get_path("server.ssl").unwrap().as_bool(), Some(true));
674    /// ```
675    pub fn merge(&mut self, other: Self) {
676        match (self, other) {
677            (Self::Mapping(base), Self::Mapping(other)) => {
678                for (key, other_value) in other {
679                    match base.get_mut(&key) {
680                        Some(base_value) => {
681                            base_value.merge(other_value);
682                        }
683                        None => {
684                            let _ = base.insert(key, other_value);
685                        }
686                    }
687                }
688            }
689            (this, other) => {
690                *this = other;
691            }
692        }
693    }
694
695    /// Deep merge with sequence concatenation.
696    ///
697    /// Similar to [`merge`](Self::merge), but sequences are concatenated
698    /// instead of replaced.
699    ///
700    /// # Examples
701    ///
702    /// ```rust
703    /// use noyalib::{from_str, Value};
704    ///
705    /// let mut base: Value = from_str("items:\n  - a\n  - b\n").unwrap();
706    /// let other: Value = from_str("items:\n  - c\n  - d\n").unwrap();
707    ///
708    /// base.merge_concat(other);
709    ///
710    /// let items = base.get("items").unwrap().as_sequence().unwrap();
711    /// assert_eq!(items.len(), 4);
712    /// ```
713    pub fn merge_concat(&mut self, other: Self) {
714        match (self, other) {
715            (Self::Mapping(base), Self::Mapping(other)) => {
716                for (key, other_value) in other {
717                    match base.get_mut(&key) {
718                        Some(base_value) => {
719                            base_value.merge_concat(other_value);
720                        }
721                        None => {
722                            let _ = base.insert(key, other_value);
723                        }
724                    }
725                }
726            }
727            (Self::Sequence(base), Self::Sequence(other)) => {
728                base.extend(other);
729            }
730            (this, other) => {
731                *this = other;
732            }
733        }
734    }
735
736    /// Remove a key from a mapping.
737    ///
738    /// Returns the removed value if the key existed and this is a mapping.
739    ///
740    /// # Examples
741    ///
742    /// ```rust
743    /// use noyalib::{from_str, Value};
744    ///
745    /// let mut value: Value = from_str("a: 1\nb: 2\n").unwrap();
746    /// let removed = value.remove("a");
747    ///
748    /// assert_eq!(removed.unwrap().as_i64(), Some(1));
749    /// assert!(value.get("a").is_none());
750    /// ```
751    pub fn remove(&mut self, key: &str) -> Option<Self> {
752        match self {
753            Self::Mapping(map) => map.shift_remove(key),
754            _ => None,
755        }
756    }
757
758    /// Insert a key-value pair into a mapping.
759    ///
760    /// Returns the previous value if the key existed. Returns `None` if this is
761    /// not a mapping.
762    ///
763    /// # Examples
764    ///
765    /// ```rust
766    /// use noyalib::{from_str, Value};
767    ///
768    /// let mut value: Value = from_str("a: 1\n").unwrap();
769    /// value.insert("b", Value::from(2));
770    ///
771    /// assert_eq!(value.get("b").unwrap().as_i64(), Some(2));
772    /// ```
773    pub fn insert(&mut self, key: impl Into<String>, value: Self) -> Option<Self> {
774        match self {
775            Self::Mapping(map) => map.insert(key.into(), value),
776            _ => None,
777        }
778    }
779
780    /// Performs merging of `<<` keys into the surrounding mapping.
781    ///
782    /// This implements YAML's merge key functionality as described in
783    /// <https://yaml.org/type/merge.html>.
784    ///
785    /// The merge key `<<` is used to indicate that all the keys of one or more
786    /// specified mappings should be inserted into the current mapping. If a key
787    /// already exists in the current mapping, its value is NOT overridden.
788    ///
789    /// # Examples
790    ///
791    /// ```rust
792    /// use noyalib::{from_str, Value};
793    ///
794    /// let config = r#"
795    /// defaults: &defaults
796    ///   timeout: 30
797    ///   retries: 3
798    ///
799    /// server:
800    ///   <<: *defaults
801    ///   host: localhost
802    ///   timeout: 60
803    /// "#;
804    ///
805    /// let mut value: Value = from_str(config).unwrap();
806    /// value.apply_merge().unwrap();
807    ///
808    /// // The server mapping now has merged values from defaults
809    /// assert_eq!(value["server"]["host"].as_str(), Some("localhost"));
810    /// assert_eq!(value["server"]["timeout"].as_i64(), Some(60)); // Not overridden
811    /// assert_eq!(value["server"]["retries"].as_i64(), Some(3));  // Merged from defaults
812    /// ```
813    ///
814    /// # Multiple Merge Sources
815    ///
816    /// When `<<` is followed by a sequence of mappings, they are merged in
817    /// order. Earlier mappings in the sequence take precedence for
818    /// duplicate keys.
819    ///
820    /// ```rust
821    /// use noyalib::{from_str, Value};
822    ///
823    /// let yaml = r#"
824    /// a: &a
825    ///   x: 1
826    /// b: &b
827    ///   x: 2
828    ///   y: 2
829    /// merged:
830    ///   <<: [*a, *b]
831    ///   z: 3
832    /// "#;
833    ///
834    /// let mut value: Value = from_str(yaml).unwrap();
835    /// value.apply_merge().unwrap();
836    ///
837    /// assert_eq!(value["merged"]["x"].as_i64(), Some(1)); // From *a (first)
838    /// assert_eq!(value["merged"]["y"].as_i64(), Some(2)); // From *b
839    /// assert_eq!(value["merged"]["z"].as_i64(), Some(3)); // Direct value
840    /// ```
841    ///
842    /// # Errors
843    ///
844    /// Returns an error if:
845    /// - A merge key value is a scalar (not a mapping or sequence of mappings)
846    /// - A merge key value is a tagged value
847    /// - A sequence in a merge key contains non-mapping values
848    pub fn apply_merge(&mut self) -> crate::Result<()> {
849        match self {
850            Self::Mapping(mapping) => {
851                // First, recursively apply merge to all values
852                for value in mapping.values_mut() {
853                    value.apply_merge()?;
854                }
855
856                // Then process the << key if present
857                let merge_value = mapping.remove("<<");
858                let merge_sequence = match merge_value {
859                    Some(Self::Sequence(seq)) => seq,
860                    Some(value) => vec![value],
861                    None => vec![],
862                };
863
864                // Process each merge source
865                for value in merge_sequence {
866                    match value {
867                        Self::Mapping(merge_map) => {
868                            // Merge keys from source, but don't override existing keys
869                            for (k, v) in merge_map {
870                                let _ = mapping.entry(k).or_insert(v);
871                            }
872                        }
873                        Self::Sequence(_) => {
874                            return Err(crate::Error::SequenceInMergeElement);
875                        }
876                        Self::Tagged(_) => {
877                            return Err(crate::Error::TaggedInMerge);
878                        }
879                        _ => {
880                            return Err(crate::Error::ScalarInMergeElement);
881                        }
882                    }
883                }
884            }
885            Self::Sequence(seq) => {
886                // Recursively apply merge to sequence elements
887                for value in seq {
888                    value.apply_merge()?;
889                }
890            }
891            Self::Tagged(tagged) => {
892                // Recursively apply merge to tagged value
893                tagged.value_mut().apply_merge()?;
894            }
895            // Scalars don't need merge processing
896            _ => {}
897        }
898
899        Ok(())
900    }
901
902    /// Substitute every `${name}` reference inside string scalars
903    /// with the corresponding entry from `properties`. The walk is
904    /// recursive — strings nested inside sequences, mappings, and
905    /// tagged values are all visited.
906    ///
907    /// String keys in mappings are treated as opaque and never
908    /// interpolated; only string *values* are touched. This avoids
909    /// surprising key-rename interactions and keeps the schema
910    /// stable.
911    ///
912    /// `${{` and `}}` escape sequences let users include literal
913    /// `${` and `}` in a scalar that should not be interpreted as
914    /// an interpolation site.
915    ///
916    /// # Errors
917    ///
918    /// Returns `Error::Custom` with the offending placeholder name
919    /// when a `${name}` reference is not present in `properties`.
920    /// Use [`Value::interpolate_properties_lossy`] to substitute an
921    /// empty string for unknown placeholders without erroring.
922    ///
923    /// # Examples
924    ///
925    /// ```
926    /// use noyalib::{from_str, Value};
927    /// use std::collections::HashMap;
928    ///
929    /// let mut value: Value = from_str("\
930    /// service:
931    ///   name: ${APP_NAME}
932    ///   port: ${BIND_PORT}
933    /// ").unwrap();
934    ///
935    /// let mut props = HashMap::new();
936    /// props.insert("APP_NAME".to_string(), "noyalib".to_string());
937    /// props.insert("BIND_PORT".to_string(), "8080".to_string());
938    ///
939    /// value.interpolate_properties(&props).unwrap();
940    /// assert_eq!(value["service"]["name"].as_str(), Some("noyalib"));
941    /// // The numeric value stays as a string — re-deserialize the
942    /// // tree if you need typed coercion.
943    /// assert_eq!(value["service"]["port"].as_str(), Some("8080"));
944    /// ```
945    #[cfg(feature = "std")]
946    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
947    pub fn interpolate_properties<S>(
948        &mut self,
949        properties: &std::collections::HashMap<String, S>,
950    ) -> crate::Result<()>
951    where
952        S: AsRef<str>,
953    {
954        self.interpolate_inner(
955            &|name| match properties.get(name) {
956                Some(v) => ResolveOutcome::Found(v.as_ref().to_owned()),
957                None => ResolveOutcome::Missing,
958            },
959            MissingAction::Error(false),
960        )
961    }
962
963    /// Like [`Value::interpolate_properties`] but redacts the
964    /// placeholder name from any error surfaced when an unknown
965    /// `${name}` is encountered — useful when the placeholder
966    /// name itself is sensitive (e.g. it carries an audit-trail
967    /// secret identifier, or it's used in a context where logs
968    /// are externally indexed).
969    ///
970    /// On success this method is identical to
971    /// `interpolate_properties`. On failure the error reads
972    /// `interpolate_properties: unknown placeholder
973    /// ${"<redacted>"}` instead of including the original name.
974    /// Substituted *values* (the contents of the property map)
975    /// are never echoed to errors — that's the responsibility of
976    /// the caller's downstream validators.
977    ///
978    /// # Examples
979    ///
980    /// ```
981    /// use noyalib::{from_str, Value};
982    /// use std::collections::HashMap;
983    ///
984    /// let mut value: Value = from_str("token: ${SECRET_TOKEN_NAME}").unwrap();
985    /// // Empty property map — substitution fails. With the
986    /// // redacting variant the placeholder name does not leak.
987    /// let props: HashMap<String, String> = HashMap::new();
988    /// let err = value.interpolate_properties_redacted(&props).unwrap_err();
989    /// let msg = err.to_string();
990    /// assert!(msg.contains("<redacted>"));
991    /// assert!(!msg.contains("SECRET_TOKEN_NAME"));
992    /// ```
993    #[cfg(feature = "std")]
994    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
995    pub fn interpolate_properties_redacted<S>(
996        &mut self,
997        properties: &std::collections::HashMap<String, S>,
998    ) -> crate::Result<()>
999    where
1000        S: AsRef<str>,
1001    {
1002        self.interpolate_inner(
1003            &|name| match properties.get(name) {
1004                Some(v) => ResolveOutcome::Found(v.as_ref().to_owned()),
1005                None => ResolveOutcome::Missing,
1006            },
1007            MissingAction::Error(true),
1008        )
1009    }
1010
1011    /// Like [`Value::interpolate_properties`] but never errors —
1012    /// unknown placeholders are replaced with an empty string. The
1013    /// motivating use case is environment-variable expansion where
1014    /// missing variables should silently degrade rather than abort
1015    /// the load.
1016    ///
1017    /// # Examples
1018    ///
1019    /// ```
1020    /// use noyalib::{from_str, Value};
1021    /// use std::collections::HashMap;
1022    ///
1023    /// let mut value: Value = from_str("greeting: hello ${WHO}, hello ${MISSING}").unwrap();
1024    /// let mut props: HashMap<String, String> = HashMap::new();
1025    /// props.insert("WHO".into(), "world".into());
1026    ///
1027    /// value.interpolate_properties_lossy(&props);
1028    /// assert_eq!(value["greeting"].as_str(), Some("hello world, hello "));
1029    /// ```
1030    #[cfg(feature = "std")]
1031    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
1032    pub fn interpolate_properties_lossy<S>(
1033        &mut self,
1034        properties: &std::collections::HashMap<String, S>,
1035    ) where
1036        S: AsRef<str>,
1037    {
1038        // Missing entries fall back to the empty string (and
1039        // honour any `${name:-default}` syntax along the way).
1040        // The walk never errors so the outer call is total.
1041        let _ = self.interpolate_inner(
1042            &|name| match properties.get(name) {
1043                Some(v) => ResolveOutcome::Found(v.as_ref().to_owned()),
1044                None => ResolveOutcome::Missing,
1045            },
1046            MissingAction::Empty,
1047        );
1048    }
1049
1050    /// Internal interpolation driver — `resolve` returns a
1051    /// [`ResolveOutcome`] tri-state so the walker can distinguish
1052    /// found / missing / errored. `missing_action` decides what
1053    /// happens when a placeholder is missing *and* has no
1054    /// `:-default` fallback.
1055    #[cfg(feature = "std")]
1056    pub(crate) fn interpolate_inner(
1057        &mut self,
1058        resolve: &dyn Fn(&str) -> ResolveOutcome,
1059        missing_action: MissingAction,
1060    ) -> crate::Result<()> {
1061        self.interpolate_inner_bounded(resolve, missing_action, None)
1062    }
1063
1064    /// Bounded counterpart to [`Self::interpolate_inner`] that charges
1065    /// post-expansion scalar and key byte lengths against `max_scalar_bytes`.
1066    #[cfg(feature = "std")]
1067    pub(crate) fn interpolate_inner_bounded(
1068        &mut self,
1069        resolve: &dyn Fn(&str) -> ResolveOutcome,
1070        missing_action: MissingAction,
1071        max_scalar_bytes: Option<usize>,
1072    ) -> crate::Result<()> {
1073        let mut observed = 0usize;
1074        self.interpolate_walk(resolve, missing_action, max_scalar_bytes, &mut observed)
1075    }
1076
1077    #[cfg(feature = "std")]
1078    fn interpolate_walk(
1079        &mut self,
1080        resolve: &dyn Fn(&str) -> ResolveOutcome,
1081        missing_action: MissingAction,
1082        max_scalar_bytes: Option<usize>,
1083        observed: &mut usize,
1084    ) -> crate::Result<()> {
1085        match self {
1086            Self::String(s) => {
1087                if let Some(updated) =
1088                    expand_placeholders(s, resolve, missing_action, max_scalar_bytes, *observed)?
1089                {
1090                    *s = updated;
1091                }
1092                *observed = observed.saturating_add(s.len());
1093                check_interpolated_scalar_budget(*observed, max_scalar_bytes)?;
1094            }
1095            Self::Sequence(seq) => {
1096                for v in seq {
1097                    v.interpolate_walk(resolve, missing_action, max_scalar_bytes, observed)?;
1098                }
1099            }
1100            Self::Mapping(map) => {
1101                for (k, v) in map.iter_mut() {
1102                    *observed = observed.saturating_add(k.len());
1103                    check_interpolated_scalar_budget(*observed, max_scalar_bytes)?;
1104                    v.interpolate_walk(resolve, missing_action, max_scalar_bytes, observed)?;
1105                }
1106            }
1107            Self::Tagged(tagged) => {
1108                tagged.value_mut().interpolate_walk(
1109                    resolve,
1110                    missing_action,
1111                    max_scalar_bytes,
1112                    observed,
1113                )?;
1114            }
1115            // Null / Bool / Number have no string content; nothing to do.
1116            Self::Null | Self::Bool(_) | Self::Number(_) => {}
1117        }
1118        Ok(())
1119    }
1120
1121    /// Recursively strips tags from this value, returning the untagged value.
1122    ///
1123    /// If the value is `Value::Tagged`, the inner value is returned
1124    /// (recursively untagged). Sequences and mappings have their elements
1125    /// recursively untagged.
1126    ///
1127    /// # Examples
1128    ///
1129    /// ```
1130    /// use noyalib::{from_str, Value};
1131    /// let v: Value = from_str("!Custom 'hi'\n").unwrap();
1132    /// assert!(v.is_tagged());
1133    /// let untagged = v.untag();
1134    /// assert_eq!(untagged.as_str(), Some("hi"));
1135    /// ```
1136    #[must_use]
1137    pub fn untag(self) -> Self {
1138        match self {
1139            Self::Tagged(tagged) => tagged.value.untag(),
1140            Self::Sequence(seq) => Self::Sequence(seq.into_iter().map(Self::untag).collect()),
1141            Self::Mapping(map) => {
1142                let untagged: Mapping = map.into_iter().map(|(k, v)| (k, v.untag())).collect();
1143                Self::Mapping(untagged)
1144            }
1145            other => other,
1146        }
1147    }
1148
1149    /// Returns a reference to the innermost untagged value.
1150    ///
1151    /// If the value is `Value::Tagged`, returns a reference to the inner value
1152    /// (recursively following tags). Does not recurse into sequences or
1153    /// mappings.
1154    ///
1155    /// # Examples
1156    ///
1157    /// ```
1158    /// use noyalib::{from_str, Value};
1159    /// let v: Value = from_str("!Custom 'hi'\n").unwrap();
1160    /// assert_eq!(v.untag_ref().as_str(), Some("hi"));
1161    /// ```
1162    #[must_use]
1163    pub fn untag_ref(&self) -> &Self {
1164        match self {
1165            Self::Tagged(tagged) => tagged.value.untag_ref(),
1166            other => other,
1167        }
1168    }
1169
1170    /// Returns a mutable reference to the innermost untagged value.
1171    ///
1172    /// If the value is `Value::Tagged`, returns a mutable reference to the
1173    /// inner value (recursively following tags). Does not recurse into
1174    /// sequences or mappings.
1175    ///
1176    /// # Examples
1177    ///
1178    /// ```
1179    /// use noyalib::{from_str, Value};
1180    /// let mut v: Value = from_str("!Custom 'hi'\n").unwrap();
1181    /// *v.untag_mut() = Value::from("bye");
1182    /// assert_eq!(v.untag_ref().as_str(), Some("bye"));
1183    /// ```
1184    #[must_use]
1185    pub fn untag_mut(&mut self) -> &mut Self {
1186        match self {
1187            Self::Tagged(tagged) => tagged.value.untag_mut(),
1188            other => other,
1189        }
1190    }
1191}
1192
1193// Use shared path parsing from the path module.
1194use crate::path::{QueryPath, QuerySegment, parse_query_path};
1195
1196/// Backwards-compatible alias.
1197fn parse_path(path: &str) -> Vec<QuerySegment> {
1198    parse_query_path(path)
1199}
1200
1201/// Recursively query a Value tree against path segments.
1202fn query_recursive<'a>(
1203    value: &'a Value,
1204    segments: &[QuerySegment],
1205    depth: usize,
1206    results: &mut Vec<&'a Value>,
1207) {
1208    if depth >= segments.len() {
1209        results.push(value);
1210        return;
1211    }
1212
1213    match &segments[depth] {
1214        QuerySegment::Key(key) => {
1215            if let Some(child) = value.get(key.as_str()) {
1216                query_recursive(child, segments, depth + 1, results);
1217            }
1218        }
1219        QuerySegment::Index(idx) => {
1220            if let Some(child) = value.get(*idx) {
1221                query_recursive(child, segments, depth + 1, results);
1222            }
1223        }
1224        QuerySegment::Wildcard => match value {
1225            Value::Sequence(seq) => {
1226                for item in seq {
1227                    query_recursive(item, segments, depth + 1, results);
1228                }
1229            }
1230            Value::Mapping(map) => {
1231                for (_, v) in map {
1232                    query_recursive(v, segments, depth + 1, results);
1233                }
1234            }
1235            _ => {}
1236        },
1237        QuerySegment::RecursiveDescent => {
1238            // Match the remaining path at this level and all descendants
1239            let remaining = &segments[depth + 1..];
1240            if !remaining.is_empty() {
1241                // Try matching the rest of the path at this node
1242                query_recursive(value, segments, depth + 1, results);
1243                // Recurse into all children
1244                match value {
1245                    Value::Sequence(seq) => {
1246                        for item in seq {
1247                            query_recursive(item, segments, depth, results);
1248                        }
1249                    }
1250                    Value::Mapping(map) => {
1251                        for (_, v) in map {
1252                            query_recursive(v, segments, depth, results);
1253                        }
1254                    }
1255                    Value::Tagged(t) => {
1256                        query_recursive(t.value(), segments, depth, results);
1257                    }
1258                    _ => {}
1259                }
1260            }
1261        }
1262    }
1263}
1264
1265impl PartialEq for Value {
1266    fn eq(&self, other: &Self) -> bool {
1267        match (self, other) {
1268            (Self::Null, Self::Null) => true,
1269            (Self::Bool(a), Self::Bool(b)) => a == b,
1270            (Self::Number(a), Self::Number(b)) => a == b,
1271            (Self::String(a), Self::String(b)) => a == b,
1272            (Self::Sequence(a), Self::Sequence(b)) => a == b,
1273            (Self::Mapping(a), Self::Mapping(b)) => a == b,
1274            (Self::Tagged(a), Self::Tagged(b)) => a == b,
1275            _ => false,
1276        }
1277    }
1278}
1279
1280impl Eq for Value {}
1281
1282impl Hash for Value {
1283    fn hash<H: Hasher>(&self, state: &mut H) {
1284        // Discriminant for variant type
1285        core::mem::discriminant(self).hash(state);
1286        match self {
1287            Self::Null => {}
1288            Self::Bool(b) => b.hash(state),
1289            Self::Number(n) => n.hash(state),
1290            Self::String(s) => s.hash(state),
1291            Self::Sequence(seq) => {
1292                seq.len().hash(state);
1293                for v in seq {
1294                    v.hash(state);
1295                }
1296            }
1297            Self::Mapping(map) => {
1298                map.len().hash(state);
1299                for (k, v) in map {
1300                    k.hash(state);
1301                    v.hash(state);
1302                }
1303            }
1304            Self::Tagged(tagged) => {
1305                tagged.tag().hash(state);
1306                tagged.value().hash(state);
1307            }
1308        }
1309    }
1310}
1311
1312impl PartialOrd for Value {
1313    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1314        Some(self.cmp(other))
1315    }
1316}
1317
1318impl Ord for Value {
1319    fn cmp(&self, other: &Self) -> Ordering {
1320        // Order: Null < Bool < Number < String < Sequence < Mapping < Tagged
1321        fn type_order(v: &Value) -> u8 {
1322            match v {
1323                Value::Null => 0,
1324                Value::Bool(_) => 1,
1325                Value::Number(_) => 2,
1326                Value::String(_) => 3,
1327                Value::Sequence(_) => 4,
1328                Value::Mapping(_) => 5,
1329                Value::Tagged(_) => 6,
1330            }
1331        }
1332
1333        match type_order(self).cmp(&type_order(other)) {
1334            Ordering::Equal => {}
1335            ord => return ord,
1336        }
1337
1338        match (self, other) {
1339            (Self::Null, Self::Null) => Ordering::Equal,
1340            (Self::Bool(a), Self::Bool(b)) => a.cmp(b),
1341            (Self::Number(a), Self::Number(b)) => a.cmp(b),
1342            (Self::String(a), Self::String(b)) => a.cmp(b),
1343            (Self::Sequence(a), Self::Sequence(b)) => a.len().cmp(&b.len()).then_with(|| {
1344                for (av, bv) in a.iter().zip(b.iter()) {
1345                    match av.cmp(bv) {
1346                        Ordering::Equal => continue,
1347                        ord => return ord,
1348                    }
1349                }
1350                Ordering::Equal
1351            }),
1352            (Self::Mapping(a), Self::Mapping(b)) => a.len().cmp(&b.len()).then_with(|| {
1353                for ((ak, av), (bk, bv)) in a.iter().zip(b.iter()) {
1354                    match ak.cmp(bk) {
1355                        Ordering::Equal => {}
1356                        ord => return ord,
1357                    }
1358                    match av.cmp(bv) {
1359                        Ordering::Equal => continue,
1360                        ord => return ord,
1361                    }
1362                }
1363                Ordering::Equal
1364            }),
1365            (Self::Tagged(a), Self::Tagged(b)) => a
1366                .tag()
1367                .as_str()
1368                .cmp(b.tag().as_str())
1369                .then_with(|| a.value().cmp(b.value())),
1370            _ => unreachable!("type_order check ensures same variants"),
1371        }
1372    }
1373}
1374
1375impl fmt::Display for Value {
1376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1377        match self {
1378            Self::Null => write!(f, "null"),
1379            Self::Bool(b) => write!(f, "{b}"),
1380            Self::Number(n) => write!(f, "{n}"),
1381            Self::String(s) => write!(f, "{s}"),
1382            Self::Sequence(s) => {
1383                write!(f, "[")?;
1384                for (i, v) in s.iter().enumerate() {
1385                    if i > 0 {
1386                        write!(f, ", ")?;
1387                    }
1388                    write!(f, "{v}")?;
1389                }
1390                write!(f, "]")
1391            }
1392            Self::Mapping(m) => {
1393                write!(f, "{{")?;
1394                for (i, (k, v)) in m.iter().enumerate() {
1395                    if i > 0 {
1396                        write!(f, ", ")?;
1397                    }
1398                    write!(f, "{k}: {v}")?;
1399                }
1400                write!(f, "}}")
1401            }
1402            Self::Tagged(t) => write!(f, "{t}"),
1403        }
1404    }
1405}
1406
1407/// A type that can be used to index into a `Value`.
1408///
1409/// This trait provides methods for accessing elements within a [`Value`] by
1410/// index. It is implemented for:
1411/// - `usize` - for indexing into sequences
1412/// - `&str` - for indexing into mappings by string key
1413/// - `String` - for indexing into mappings by owned string
1414/// - `&String` - for indexing into mappings by string reference
1415///
1416/// # Examples
1417///
1418/// ```rust
1419/// use noyalib::{from_str, Value, ValueIndex};
1420///
1421/// let yaml = r#"
1422/// items:
1423///   - name: first
1424///   - name: second
1425/// config:
1426///   host: localhost
1427/// "#;
1428///
1429/// let value: Value = from_str(yaml).unwrap();
1430///
1431/// // Using usize to index into sequences
1432/// assert_eq!(
1433///     value
1434///         .get("items")
1435///         .unwrap()
1436///         .get(0)
1437///         .unwrap()
1438///         .get("name")
1439///         .unwrap()
1440///         .as_str(),
1441///     Some("first")
1442/// );
1443///
1444/// // Using &str to index into mappings
1445/// assert_eq!(
1446///     value.get("config").unwrap().get("host").unwrap().as_str(),
1447///     Some("localhost")
1448/// );
1449/// ```
1450pub trait ValueIndex {
1451    /// Index into a value, returning a reference to the element if found.
1452    ///
1453    /// Returns `None` if:
1454    /// - The value is not the appropriate type for this index (e.g., indexing a
1455    ///   mapping with `usize`)
1456    /// - The index/key doesn't exist
1457    fn index_into(self, value: &Value) -> Option<&Value>;
1458
1459    /// Mutably index into a value, returning a mutable reference to the element
1460    /// if found.
1461    ///
1462    /// Returns `None` if:
1463    /// - The value is not the appropriate type for this index
1464    /// - The index/key doesn't exist
1465    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value>;
1466
1467    /// Index into a value, inserting a default value if the key doesn't exist.
1468    ///
1469    /// This method is useful for building nested structures or ensuring a key
1470    /// exists.
1471    ///
1472    /// # Behavior
1473    ///
1474    /// - For sequences: panics if the index is out of bounds
1475    /// - For mappings: creates a null entry if the key doesn't exist
1476    /// - For null values: converts to an empty mapping (for string keys only)
1477    ///
1478    /// # Panics
1479    ///
1480    /// Panics if:
1481    /// - The value is not the appropriate type for this index
1482    /// - Indexing a sequence with an out-of-bounds index
1483    fn index_or_insert(self, value: &mut Value) -> &mut Value;
1484}
1485
1486impl ValueIndex for usize {
1487    fn index_into(self, value: &Value) -> Option<&Value> {
1488        match value {
1489            Value::Sequence(s) => s.get(self),
1490            Value::Tagged(t) => self.index_into(t.value()),
1491            _ => None,
1492        }
1493    }
1494
1495    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value> {
1496        match value {
1497            Value::Sequence(s) => s.get_mut(self),
1498            Value::Tagged(t) => self.index_into_mut(t.value_mut()),
1499            _ => None,
1500        }
1501    }
1502
1503    #[track_caller]
1504    fn index_or_insert(self, value: &mut Value) -> &mut Value {
1505        match value {
1506            Value::Sequence(s) => {
1507                let len = s.len();
1508                s.get_mut(self).unwrap_or_else(|| {
1509                    panic!("cannot access index {self} of YAML sequence of length {len}")
1510                })
1511            }
1512            Value::Tagged(t) => self.index_or_insert(t.value_mut()),
1513            _ => panic!(
1514                "cannot access index {} of YAML {}",
1515                self,
1516                value_type_name(value)
1517            ),
1518        }
1519    }
1520}
1521
1522impl ValueIndex for &str {
1523    fn index_into(self, value: &Value) -> Option<&Value> {
1524        match value {
1525            Value::Mapping(m) => m.get(self),
1526            Value::Tagged(t) => self.index_into(t.value()),
1527            _ => None,
1528        }
1529    }
1530
1531    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value> {
1532        match value {
1533            Value::Mapping(m) => m.get_mut(self),
1534            Value::Tagged(t) => self.index_into_mut(t.value_mut()),
1535            _ => None,
1536        }
1537    }
1538
1539    #[track_caller]
1540    fn index_or_insert(self, value: &mut Value) -> &mut Value {
1541        // If the value is null, convert it to an empty mapping
1542        if matches!(value, Value::Null) {
1543            *value = Value::Mapping(Mapping::new());
1544        }
1545
1546        match value {
1547            Value::Mapping(m) => {
1548                let _ = m.entry(self.to_owned()).or_insert(Value::Null);
1549                m.get_mut(self).unwrap()
1550            }
1551            Value::Tagged(t) => self.index_or_insert(t.value_mut()),
1552            _ => panic!(
1553                "cannot access key {:?} in YAML {}",
1554                self,
1555                value_type_name(value)
1556            ),
1557        }
1558    }
1559}
1560
1561impl ValueIndex for String {
1562    fn index_into(self, value: &Value) -> Option<&Value> {
1563        self.as_str().index_into(value)
1564    }
1565
1566    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value> {
1567        self.as_str().index_into_mut(value)
1568    }
1569
1570    #[track_caller]
1571    fn index_or_insert(self, value: &mut Value) -> &mut Value {
1572        self.as_str().index_or_insert(value)
1573    }
1574}
1575
1576impl ValueIndex for &String {
1577    fn index_into(self, value: &Value) -> Option<&Value> {
1578        self.as_str().index_into(value)
1579    }
1580
1581    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value> {
1582        self.as_str().index_into_mut(value)
1583    }
1584
1585    #[track_caller]
1586    fn index_or_insert(self, value: &mut Value) -> &mut Value {
1587        self.as_str().index_or_insert(value)
1588    }
1589}
1590
1591impl ValueIndex for &Value {
1592    fn index_into(self, value: &Value) -> Option<&Value> {
1593        match self {
1594            Value::String(s) => s.as_str().index_into(value),
1595            Value::Number(Number::Integer(n)) if *n >= 0 => {
1596                usize::try_from(*n).ok()?.index_into(value)
1597            }
1598            _ => None,
1599        }
1600    }
1601
1602    fn index_into_mut(self, value: &mut Value) -> Option<&mut Value> {
1603        match self {
1604            Value::String(s) => s.as_str().index_into_mut(value),
1605            Value::Number(Number::Integer(n)) if *n >= 0 => {
1606                usize::try_from(*n).ok()?.index_into_mut(value)
1607            }
1608            _ => None,
1609        }
1610    }
1611
1612    #[track_caller]
1613    fn index_or_insert(self, value: &mut Value) -> &mut Value {
1614        match self {
1615            Value::String(s) => s.as_str().index_or_insert(value),
1616            Value::Number(Number::Integer(n)) if *n >= 0 => {
1617                let idx =
1618                    usize::try_from(*n).unwrap_or_else(|_| panic!("index {n} overflows usize"));
1619                idx.index_or_insert(value)
1620            }
1621            _ => panic!("cannot index with {self:?}"),
1622        }
1623    }
1624}
1625
1626/// Expand `${name}` placeholders in `s` using the supplied
1627/// resolver. Returns:
1628///
1629/// - `Ok(None)` when no placeholders are present (caller can avoid
1630///   allocating a fresh `String`),
1631/// - `Ok(Some(expanded))` when at least one placeholder was found,
1632/// - `Err(_)` when the resolver returned an error for a
1633///   placeholder.
1634///
1635/// Escape sequences:
1636/// - `${{` produces a literal `${` (placeholder NOT recognised),
1637/// - `}}` produces a literal `}`.
1638///
1639/// Placeholder names match `[A-Za-z_][A-Za-z0-9_.]*` — letters,
1640/// digits, underscore, dot. The dot allows hierarchical names like
1641/// `${db.host}` for users who want to namespace their property
1642/// maps. Anything that does not match is a parse error.
1643/// Outcome of resolving a placeholder name.
1644///
1645/// Three-way return so [`expand_placeholders`] can distinguish a
1646/// genuinely missing key (which may then defer to a `:-default`
1647/// fallback) from a key whose own resolution errored.
1648#[cfg(feature = "std")]
1649pub(crate) enum ResolveOutcome {
1650    /// Name resolved cleanly.
1651    Found(String),
1652    /// Name not in the resolver's lookup table.
1653    Missing,
1654    /// Resolution failed for a non-missing reason. Reserved for
1655    /// future resolver impls that may surface I/O or permissions
1656    /// errors (e.g. environment-variable-backed resolvers); not
1657    /// produced by the in-memory `properties` map path today.
1658    #[allow(dead_code)]
1659    Error(crate::Error),
1660}
1661
1662/// What to do when a placeholder has no map entry and no
1663/// `:-default` fallback.
1664#[cfg(feature = "std")]
1665#[derive(Debug, Clone, Copy)]
1666pub(crate) enum MissingAction {
1667    /// Substitute the empty string. Lossy/non-strict mode.
1668    Empty,
1669    /// Surface an error. Boolean is `true` to redact the
1670    /// placeholder name from the error message.
1671    Error(bool),
1672}
1673
1674#[cfg(feature = "std")]
1675#[inline]
1676fn check_interpolated_scalar_budget(
1677    observed: usize,
1678    max_scalar_bytes: Option<usize>,
1679) -> crate::Result<()> {
1680    if let Some(limit) = max_scalar_bytes
1681        && observed > limit
1682    {
1683        return Err(crate::Error::Budget(
1684            crate::BudgetBreach::MaxTotalScalarBytes { limit, observed },
1685        ));
1686    }
1687    Ok(())
1688}
1689
1690#[cfg(feature = "std")]
1691fn parse_placeholder_token(
1692    s: &str,
1693    name_start: usize,
1694) -> crate::Result<(&str, Option<&str>, usize)> {
1695    let bytes = s.as_bytes();
1696    let mut j = name_start;
1697    while j < bytes.len() && bytes[j] != b'}' && bytes[j] != b':' {
1698        let c = bytes[j];
1699        let ok = c.is_ascii_alphanumeric() || c == b'_' || c == b'.';
1700        if !ok {
1701            return Err(crate::Error::Custom(format!(
1702                "interpolate_properties: invalid character {:?} in placeholder",
1703                c as char
1704            )));
1705        }
1706        j += 1;
1707    }
1708    if j >= bytes.len() {
1709        return Err(crate::Error::Custom(
1710            "interpolate_properties: unterminated `${...}` placeholder".into(),
1711        ));
1712    }
1713    if name_start == j {
1714        return Err(crate::Error::Custom(
1715            "interpolate_properties: empty placeholder `${}`".into(),
1716        ));
1717    }
1718    let name = &s[name_start..j];
1719    let mut default: Option<&str> = None;
1720    let mut close = j;
1721    if bytes[j] == b':' {
1722        if j + 1 >= bytes.len() || bytes[j + 1] != b'-' {
1723            return Err(crate::Error::Custom(
1724                "interpolate_properties: expected `:-default` after `${name:`".into(),
1725            ));
1726        }
1727        let default_start = j + 2;
1728        let close_offset = bytes[default_start..]
1729            .iter()
1730            .position(|&b| b == b'}')
1731            .ok_or_else(|| {
1732                crate::Error::Custom(
1733                    "interpolate_properties: unterminated `${name:-default}`".into(),
1734                )
1735            })?;
1736        close = default_start + close_offset;
1737        default = Some(&s[default_start..close]);
1738    }
1739    Ok((name, default, close))
1740}
1741
1742#[cfg(feature = "std")]
1743fn resolve_placeholder_value(
1744    name: &str,
1745    default: Option<&str>,
1746    resolve: &dyn Fn(&str) -> ResolveOutcome,
1747    missing_action: MissingAction,
1748) -> crate::Result<String> {
1749    match resolve(name) {
1750        ResolveOutcome::Found(v) => Ok(v),
1751        ResolveOutcome::Missing => match default {
1752            Some(d) => Ok(d.to_owned()),
1753            None => match missing_action {
1754                MissingAction::Empty => Ok(String::new()),
1755                MissingAction::Error(redact) => Err(crate::Error::Custom(if redact {
1756                    "interpolate_properties: unknown placeholder `${<redacted>}`".into()
1757                } else {
1758                    format!("interpolate_properties: unknown placeholder `${{{name}}}`")
1759                })),
1760            },
1761        },
1762        ResolveOutcome::Error(e) => Err(e),
1763    }
1764}
1765
1766#[cfg(feature = "std")]
1767fn expand_placeholders(
1768    s: &str,
1769    resolve: &dyn Fn(&str) -> ResolveOutcome,
1770    missing_action: MissingAction,
1771    max_scalar_bytes: Option<usize>,
1772    observed_base: usize,
1773) -> crate::Result<Option<String>> {
1774    let bytes = s.as_bytes();
1775    // Fast path: no `$` and no `}` → no allocation, no walk.
1776    if !bytes.contains(&b'$') && !bytes.contains(&b'}') {
1777        return Ok(None);
1778    }
1779    let mut out = String::with_capacity(s.len());
1780    let mut i = 0;
1781    let mut touched = false;
1782    while i < bytes.len() {
1783        let b = bytes[i];
1784        if b == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'$' {
1785            // Escape: `$$` → literal `$`.
1786            out.push('$');
1787            i += 2;
1788            touched = true;
1789            continue;
1790        }
1791        if b == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1792            // Escape: `${{` → literal `${`.
1793            if i + 2 < bytes.len() && bytes[i + 2] == b'{' {
1794                out.push_str("${");
1795                i += 3;
1796                touched = true;
1797                continue;
1798            }
1799            let (name, default, close) = parse_placeholder_token(s, i + 2)?;
1800            let value = resolve_placeholder_value(name, default, resolve, missing_action)?;
1801            let next_len = observed_base
1802                .saturating_add(out.len())
1803                .saturating_add(value.len());
1804            check_interpolated_scalar_budget(next_len, max_scalar_bytes)?;
1805            out.push_str(&value);
1806            i = close + 1;
1807            touched = true;
1808            continue;
1809        }
1810        if b == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}' {
1811            // Escape: `}}` → literal `}`.
1812            out.push('}');
1813            i += 2;
1814            touched = true;
1815            continue;
1816        }
1817        // Multi-byte UTF-8 — push the leading byte's char in one go.
1818        let c = s[i..].chars().next().expect("char at boundary");
1819        out.push(c);
1820        i += c.len_utf8();
1821    }
1822    if touched { Ok(Some(out)) } else { Ok(None) }
1823}
1824
1825/// Returns the type name of a value for error messages.
1826fn value_type_name(value: &Value) -> &'static str {
1827    match value {
1828        Value::Null => "null",
1829        Value::Bool(_) => "boolean",
1830        Value::Number(_) => "number",
1831        Value::String(_) => "string",
1832        Value::Sequence(_) => "sequence",
1833        Value::Mapping(_) => "mapping",
1834        Value::Tagged(_) => "tagged value",
1835    }
1836}