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}