Skip to main content

yaml_edit/nodes/
entry.rs

1//! Entry API for [`Mapping`](crate::Mapping).
2//!
3//! Modelled on [`std::collections::BTreeMap::entry`]. Obtain an [`Entry`]
4//! via [`Mapping::entry`](crate::Mapping::entry).
5//!
6//! ```rust
7//! use std::str::FromStr;
8//! use yaml_edit::Document;
9//!
10//! let doc = Document::from_str("name: Alice\n").unwrap();
11//! let mapping = doc.as_mapping().unwrap();
12//!
13//! mapping.entry("age").or_insert(30);
14//! mapping.entry("name").or_insert("Bob");
15//!
16//! assert_eq!(doc.to_string(), "name: Alice\nage: 30\n");
17//! ```
18
19use crate::as_yaml::{AsYaml, YamlNode};
20use crate::nodes::mapping::{Mapping, MappingEntry};
21use crate::nodes::sequence::Sequence;
22
23/// A view into a single entry in a [`Mapping`], which may either be vacant
24/// or occupied.
25///
26/// Obtained from [`Mapping::entry`](crate::Mapping::entry).
27#[derive(Debug)]
28pub enum Entry<'a, K: AsYaml> {
29    /// The key is already present in the mapping.
30    Occupied(OccupiedEntry<'a>),
31    /// The key is not present in the mapping.
32    Vacant(VacantEntry<'a, K>),
33}
34
35/// A view into an occupied entry in a [`Mapping`].
36#[derive(Debug)]
37pub struct OccupiedEntry<'a> {
38    pub(crate) mapping: &'a Mapping,
39    pub(crate) entry: MappingEntry,
40}
41
42/// A view into a vacant entry in a [`Mapping`].
43#[derive(Debug)]
44pub struct VacantEntry<'a, K: AsYaml> {
45    pub(crate) mapping: &'a Mapping,
46    pub(crate) key: K,
47}
48
49impl<'a, K: AsYaml> Entry<'a, K> {
50    /// Ensure a value is in the entry by inserting `default` if vacant, and
51    /// return the resulting value as a [`YamlNode`].
52    pub fn or_insert(self, default: impl AsYaml) -> YamlNode {
53        match self {
54            Entry::Occupied(o) => o.into_value(),
55            Entry::Vacant(v) => v.insert(default),
56        }
57    }
58
59    /// Ensure a value is in the entry by inserting the result of `default` if
60    /// vacant, and return the resulting value as a [`YamlNode`].
61    ///
62    /// `default` is only called if the entry is vacant.
63    pub fn or_insert_with<V, F>(self, default: F) -> YamlNode
64    where
65        V: AsYaml,
66        F: FnOnce() -> V,
67    {
68        match self {
69            Entry::Occupied(o) => o.into_value(),
70            Entry::Vacant(v) => v.insert(default()),
71        }
72    }
73
74    /// Ensure the entry holds a [`Mapping`] and return it.
75    ///
76    /// If the entry is vacant, an empty mapping is inserted. If the entry is
77    /// occupied but the value is not a mapping, it is replaced with an empty
78    /// mapping. Otherwise the existing nested mapping is returned unchanged.
79    pub fn or_insert_mapping(self) -> Mapping {
80        match self {
81            Entry::Occupied(o) => {
82                if let Some(existing) = o.get_mapping() {
83                    return existing;
84                }
85                o.set_value(Mapping::new());
86                o.get_mapping()
87                    .expect("value was just replaced with a mapping")
88            }
89            Entry::Vacant(v) => {
90                let VacantEntry { mapping, key } = v;
91                mapping.set(&key, Mapping::new());
92                mapping
93                    .get_mapping(&key)
94                    .expect("mapping was just inserted")
95            }
96        }
97    }
98
99    /// Ensure the entry holds a [`Sequence`] and return it.
100    ///
101    /// If the entry is vacant, an empty sequence is inserted. If the entry is
102    /// occupied but the value is not a sequence, it is replaced with an empty
103    /// sequence. Otherwise the existing nested sequence is returned unchanged.
104    pub fn or_insert_sequence(self) -> Sequence {
105        match self {
106            Entry::Occupied(o) => {
107                if let Some(existing) = o.get_sequence() {
108                    return existing;
109                }
110                o.set_value(Sequence::new());
111                o.get_sequence()
112                    .expect("value was just replaced with a sequence")
113            }
114            Entry::Vacant(v) => {
115                let VacantEntry { mapping, key } = v;
116                mapping.set(&key, Sequence::new());
117                mapping
118                    .get_sequence(&key)
119                    .expect("sequence was just inserted")
120            }
121        }
122    }
123
124    /// Provide in-place access to an occupied entry before any potential
125    /// inserts.
126    ///
127    /// The closure is only called if the entry is occupied. Returns `self`
128    /// unchanged so calls can be chained with [`or_insert`](Self::or_insert)
129    /// and friends.
130    ///
131    /// The tree uses interior mutability, so the closure receives a shared
132    /// reference; `OccupiedEntry::set_value` and friends still work through
133    /// `&self`.
134    pub fn and_modify<F>(self, f: F) -> Self
135    where
136        F: FnOnce(&OccupiedEntry<'a>),
137    {
138        if let Entry::Occupied(ref o) = self {
139            f(o);
140        }
141        self
142    }
143
144    /// Return the key that this entry refers to, as a [`YamlNode`].
145    ///
146    /// For an occupied entry this is the existing key node in the tree. For
147    /// a vacant entry it is the key that would be inserted, rendered as a
148    /// fresh scalar node.
149    pub fn key(&self) -> Option<YamlNode> {
150        match self {
151            Entry::Occupied(o) => o.key(),
152            Entry::Vacant(v) => key_as_yaml_node(&v.key),
153        }
154    }
155}
156
157impl<'a> OccupiedEntry<'a> {
158    /// Return the key of this entry as a [`YamlNode`].
159    ///
160    /// Returns `None` for malformed entries with no key node.
161    pub fn key(&self) -> Option<YamlNode> {
162        self.entry.key_node()
163    }
164
165    /// Return the current value of this entry as a [`YamlNode`].
166    ///
167    /// Returns `None` for malformed entries with no value node.
168    pub fn get(&self) -> Option<YamlNode> {
169        self.entry.value_node()
170    }
171
172    /// Return the value as a nested [`Mapping`], if it is one.
173    pub fn get_mapping(&self) -> Option<Mapping> {
174        self.get().and_then(|n| n.as_mapping().cloned())
175    }
176
177    /// Return the value as a nested [`Sequence`], if it is one.
178    pub fn get_sequence(&self) -> Option<Sequence> {
179        self.get().and_then(|n| n.as_sequence().cloned())
180    }
181
182    /// Replace the value of this entry with `value`.
183    pub fn set_value(&self, value: impl AsYaml) {
184        let flow_context = self.mapping.is_flow_style();
185        self.entry.set_value(value, flow_context);
186    }
187
188    /// Remove this entry from the mapping and return the removed
189    /// [`MappingEntry`] handle. The returned entry is detached from the tree.
190    pub fn remove_entry(self) -> MappingEntry {
191        self.entry.clone().remove();
192        self.entry
193    }
194
195    /// Consume the [`OccupiedEntry`] and return the current value as a
196    /// [`YamlNode`].
197    ///
198    /// Panics if the entry has no value node (indicates malformed input).
199    pub(crate) fn into_value(self) -> YamlNode {
200        self.entry
201            .value_node()
202            .expect("occupied entry has no value node")
203    }
204}
205
206impl<'a, K: AsYaml> VacantEntry<'a, K> {
207    /// Return a reference to the pending key.
208    pub fn key(&self) -> &K {
209        &self.key
210    }
211
212    /// Consume the [`VacantEntry`] and return the key that would have been
213    /// inserted.
214    pub fn into_key(self) -> K {
215        self.key
216    }
217
218    /// Insert `value` into the mapping at this vacant entry's key, and
219    /// return the newly inserted value as a [`YamlNode`].
220    pub fn insert(self, value: impl AsYaml) -> YamlNode {
221        self.mapping.set(&self.key, value);
222        self.mapping
223            .get(&self.key)
224            .expect("value just inserted must be retrievable")
225    }
226}
227
228fn key_as_yaml_node<K: AsYaml>(key: &K) -> Option<YamlNode> {
229    use crate::lex::SyntaxKind;
230    use rowan::GreenNodeBuilder;
231
232    if let Some(node) = key.as_node() {
233        return YamlNode::from_syntax(node.clone());
234    }
235    let mut builder = GreenNodeBuilder::new();
236    builder.start_node(SyntaxKind::ROOT.into());
237    key.build_content(&mut builder, 0, false);
238    builder.finish_node();
239    let root = crate::nodes::SyntaxNode::new_root(builder.finish());
240    root.children().next().and_then(YamlNode::from_syntax)
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::{Document, Entry};
246    use std::str::FromStr;
247
248    #[test]
249    fn or_insert_on_vacant_inserts_and_returns_value() {
250        let doc = Document::from_str("name: Alice\n").unwrap();
251        let mapping = doc.as_mapping().unwrap();
252
253        let v = mapping.entry("age").or_insert(30);
254        assert_eq!(v.to_string().trim(), "30");
255        assert_eq!(doc.to_string(), "name: Alice\nage: 30\n");
256    }
257
258    #[test]
259    fn or_insert_on_occupied_returns_existing_value_and_does_not_overwrite() {
260        let doc = Document::from_str("name: Alice\n").unwrap();
261        let mapping = doc.as_mapping().unwrap();
262
263        let v = mapping.entry("name").or_insert("Bob");
264        assert_eq!(v.to_string().trim(), "Alice");
265        assert_eq!(doc.to_string(), "name: Alice\n");
266    }
267
268    #[test]
269    fn or_insert_with_only_calls_closure_when_vacant() {
270        let doc = Document::from_str("count: 5\n").unwrap();
271        let mapping = doc.as_mapping().unwrap();
272
273        let mut called = false;
274        let _ = mapping.entry("count").or_insert_with(|| {
275            called = true;
276            99_i64
277        });
278        assert!(!called);
279
280        let mut called = false;
281        let _ = mapping.entry("other").or_insert_with(|| {
282            called = true;
283            123_i64
284        });
285        assert!(called);
286        assert_eq!(doc.to_string(), "count: 5\nother: 123\n");
287    }
288
289    #[test]
290    fn and_modify_on_occupied_runs_closure_and_updates_value() {
291        let doc = Document::from_str("count: 1\n").unwrap();
292        let mapping = doc.as_mapping().unwrap();
293
294        mapping.entry("count").and_modify(|e| e.set_value(42));
295        assert_eq!(doc.to_string(), "count: 42\n");
296    }
297
298    #[test]
299    fn and_modify_on_vacant_is_noop() {
300        let doc = Document::from_str("count: 1\n").unwrap();
301        let mapping = doc.as_mapping().unwrap();
302
303        let mut called = false;
304        mapping.entry("missing").and_modify(|_| {
305            called = true;
306        });
307        assert!(!called);
308        assert_eq!(doc.to_string(), "count: 1\n");
309    }
310
311    #[test]
312    fn and_modify_then_or_insert_pattern() {
313        let doc = Document::from_str("count: 1\n").unwrap();
314        let mapping = doc.as_mapping().unwrap();
315
316        mapping
317            .entry("count")
318            .and_modify(|e| e.set_value(2))
319            .or_insert(0);
320        assert_eq!(mapping.get("count").unwrap().to_string().trim(), "2");
321
322        mapping
323            .entry("new")
324            .and_modify(|e| e.set_value("never"))
325            .or_insert("first");
326        assert_eq!(mapping.get("new").unwrap().to_string().trim(), "first");
327    }
328
329    #[test]
330    fn entry_variants_expose_expected_state() {
331        let doc = Document::from_str("k: v\n").unwrap();
332        let mapping = doc.as_mapping().unwrap();
333
334        match mapping.entry("k") {
335            Entry::Occupied(o) => {
336                assert_eq!(o.key().unwrap().to_string().trim(), "k");
337                assert_eq!(o.get().unwrap().to_string().trim(), "v");
338            }
339            Entry::Vacant(_) => panic!("expected occupied"),
340        }
341
342        match mapping.entry("missing") {
343            Entry::Vacant(v) => {
344                assert_eq!(*v.key(), "missing");
345            }
346            Entry::Occupied(_) => panic!("expected vacant"),
347        }
348    }
349
350    #[test]
351    fn entry_key_agnostic_of_variant() {
352        let doc = Document::from_str("k: v\n").unwrap();
353        let mapping = doc.as_mapping().unwrap();
354
355        let occupied_key = mapping.entry("k").key().unwrap();
356        assert_eq!(occupied_key.to_string().trim(), "k");
357
358        let vacant_key = mapping.entry("missing").key().unwrap();
359        assert_eq!(vacant_key.to_string().trim(), "missing");
360    }
361
362    #[test]
363    fn vacant_entry_insert_returns_new_value_node() {
364        let doc = Document::from_str("a: 1\n").unwrap();
365        let mapping = doc.as_mapping().unwrap();
366
367        let Entry::Vacant(v) = mapping.entry("b") else {
368            panic!("expected vacant");
369        };
370        let node = v.insert(2_i64);
371        assert_eq!(node.to_string().trim(), "2");
372        assert_eq!(doc.to_string(), "a: 1\nb: 2\n");
373    }
374
375    #[test]
376    fn occupied_entry_remove_detaches_from_mapping() {
377        let doc = Document::from_str("a: 1\nb: 2\n").unwrap();
378        let mapping = doc.as_mapping().unwrap();
379
380        let Entry::Occupied(o) = mapping.entry("a") else {
381            panic!("expected occupied");
382        };
383        let removed = o.remove_entry();
384        assert_eq!(removed.key_node().unwrap().to_string().trim(), "a");
385        assert!(!mapping.contains_key("a"));
386        assert_eq!(doc.to_string(), "b: 2\n");
387    }
388
389    #[test]
390    fn or_insert_mapping_on_vacant_creates_and_returns_nested_mapping() {
391        let doc = Document::from_str("existing: value\n").unwrap();
392        let mapping = doc.as_mapping().unwrap();
393
394        let nested = mapping.entry("nested").or_insert_mapping();
395        nested.set("inner", "value");
396        assert_eq!(
397            doc.to_string(),
398            "existing: value\nnested:\n  inner: value\n"
399        );
400    }
401
402    #[test]
403    fn or_insert_mapping_on_occupied_returns_existing_mapping() {
404        let doc = Document::from_str("root:\n  a: 1\n").unwrap();
405        let mapping = doc.as_mapping().unwrap();
406
407        let nested = mapping.entry("root").or_insert_mapping();
408        nested.set("b", 2);
409        assert_eq!(doc.to_string(), "root:\n  a: 1\n  b: 2\n");
410    }
411
412    #[test]
413    fn or_insert_sequence_on_vacant_creates_and_returns_nested_sequence() {
414        let doc = Document::from_str("existing: value\n").unwrap();
415        let mapping = doc.as_mapping().unwrap();
416
417        let list = mapping.entry("items").or_insert_sequence();
418        list.push("apple");
419        list.push("banana");
420        assert_eq!(
421            doc.to_string(),
422            "existing: value\nitems:\n  - apple\n  - banana\n"
423        );
424    }
425
426    #[test]
427    fn or_insert_sequence_on_occupied_returns_existing_sequence() {
428        let doc = Document::from_str("items:\n  - apple\n").unwrap();
429        let mapping = doc.as_mapping().unwrap();
430
431        let list = mapping.entry("items").or_insert_sequence();
432        list.push("banana");
433        assert_eq!(doc.to_string(), "items:\n  - apple\n  - banana\n");
434    }
435
436    #[test]
437    fn entry_preserves_comments_on_untouched_keys() {
438        let src = "keep: this  # inline\nother: value\n";
439        let doc = Document::from_str(src).unwrap();
440        let mapping = doc.as_mapping().unwrap();
441
442        mapping.entry("added").or_insert(1_i64);
443        assert_eq!(
444            doc.to_string(),
445            "keep: this  # inline\nother: value\nadded: 1\n"
446        );
447    }
448
449    #[test]
450    fn entry_key_semantic_equality() {
451        let doc = Document::from_str("\"name\": Alice\n").unwrap();
452        let mapping = doc.as_mapping().unwrap();
453
454        match mapping.entry("name") {
455            Entry::Occupied(_) => {}
456            Entry::Vacant(_) => panic!("expected occupied"),
457        }
458    }
459
460    #[test]
461    fn or_insert_with_can_return_different_types() {
462        let doc = Document::from_str("a: 1\n").unwrap();
463        let mapping = doc.as_mapping().unwrap();
464
465        let _ = mapping.entry("s").or_insert_with(|| "hello");
466        let _ = mapping.entry("n").or_insert_with(|| 42_i64);
467        let _ = mapping.entry("b").or_insert_with(|| true);
468        assert_eq!(doc.to_string(), "a: 1\ns: hello\nn: 42\nb: true\n");
469    }
470
471    #[test]
472    fn or_insert_mapping_replaces_non_mapping_value() {
473        let doc = Document::from_str("k: scalar\n").unwrap();
474        let mapping = doc.as_mapping().unwrap();
475
476        let nested = mapping.entry("k").or_insert_mapping();
477        nested.set("inner", "v");
478        assert_eq!(doc.to_string(), "k:\n  inner: v\n");
479    }
480}