Skip to main content

yaml_edit/
mapping_view.rs

1//! Read-only mapping interface shared by [`Mapping`] and
2//! [`MergedMapping`](crate::anchor_resolution::MergedMapping).
3//!
4//! [`MappingView`] abstracts over "anything that looks like a YAML mapping
5//! for the purposes of reading it." It lets you write code that works
6//! generically against both the underlying CST [`Mapping`] and the
7//! alias/merge-key–resolving [`MergedMapping`](crate::anchor_resolution::MergedMapping)
8//! view.
9//!
10//! Iterators are returned as boxed trait objects so the trait stays
11//! object-safe and works on the crate's MSRV. Most uses pay one heap
12//! allocation per iteration — negligible for an editing library.
13//!
14//! # Example
15//!
16//! ```
17//! use yaml_edit::{Document, MappingView};
18//! use yaml_edit::anchor_resolution::{DocumentMergedExt, MappingMergedExt};
19//! use std::str::FromStr;
20//!
21//! fn count_keys(view: &impl MappingView) -> usize {
22//!     view.keys().count()
23//! }
24//!
25//! let yaml = "\
26//! defaults: &d
27//!   timeout: 30
28//! prod:
29//!   <<: *d
30//!   host: prod.example.com
31//! ";
32//! let doc = Document::from_str(yaml).unwrap();
33//! let root = doc.as_mapping().unwrap();
34//!
35//! // Works on a plain Mapping…
36//! assert_eq!(count_keys(&root), 2);
37//!
38//! // …and on a MergedMapping.
39//! let registry = doc.merged().unwrap();
40//! let prod = registry.as_mapping().get_merged("prod").unwrap();
41//! assert_eq!(count_keys(&prod), 2); // timeout + host
42//! ```
43//!
44//! The trait is intentionally read-only: mutations only make sense on the
45//! CST [`Mapping`], so [`set`](crate::yaml::Mapping::set),
46//! [`remove`](crate::yaml::Mapping::remove), etc. remain on that type.
47
48use crate::as_yaml::YamlNode;
49
50/// A read-only "view" of a YAML mapping, implemented by both the CST
51/// [`Mapping`](crate::yaml::Mapping) and the alias/merge-key–resolving
52/// [`MergedMapping`](crate::anchor_resolution::MergedMapping).
53///
54/// See the [module docs](self) for an overview and example.
55pub trait MappingView {
56    /// Look up the value associated with `key`.
57    ///
58    /// Key matching is semantic (quoting style is ignored), so `"foo"`,
59    /// `'foo'`, and `foo` all match the scalar `foo`.
60    fn get(&self, key: &dyn crate::AsYaml) -> Option<YamlNode>;
61
62    /// Returns `true` if [`get`](Self::get) would return `Some` for `key`.
63    fn contains_key(&self, key: &dyn crate::AsYaml) -> bool {
64        self.get(key).is_some()
65    }
66
67    /// Number of entries visible through this view.
68    fn len(&self) -> usize {
69        self.iter().count()
70    }
71
72    /// Returns `true` if the view has no entries.
73    fn is_empty(&self) -> bool {
74        self.iter().next().is_none()
75    }
76
77    /// Iterate over the keys.
78    fn keys<'a>(&'a self) -> Box<dyn Iterator<Item = YamlNode> + 'a> {
79        Box::new(self.iter().map(|(k, _)| k))
80    }
81
82    /// Iterate over the values.
83    fn values<'a>(&'a self) -> Box<dyn Iterator<Item = YamlNode> + 'a> {
84        Box::new(self.iter().map(|(_, v)| v))
85    }
86
87    /// Iterate over `(key, value)` pairs.
88    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (YamlNode, YamlNode)> + 'a>;
89}
90
91impl MappingView for crate::yaml::Mapping {
92    fn get(&self, key: &dyn crate::AsYaml) -> Option<YamlNode> {
93        crate::yaml::Mapping::get(self, key)
94    }
95
96    fn contains_key(&self, key: &dyn crate::AsYaml) -> bool {
97        crate::yaml::Mapping::contains_key(self, key)
98    }
99
100    fn len(&self) -> usize {
101        crate::yaml::Mapping::len(self)
102    }
103
104    fn is_empty(&self) -> bool {
105        crate::yaml::Mapping::is_empty(self)
106    }
107
108    fn keys<'a>(&'a self) -> Box<dyn Iterator<Item = YamlNode> + 'a> {
109        Box::new(crate::yaml::Mapping::keys(self))
110    }
111
112    fn values<'a>(&'a self) -> Box<dyn Iterator<Item = YamlNode> + 'a> {
113        Box::new(crate::yaml::Mapping::values(self))
114    }
115
116    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (YamlNode, YamlNode)> + 'a> {
117        Box::new(crate::yaml::Mapping::iter(self))
118    }
119}
120
121impl MappingView for crate::anchor_resolution::MergedMapping<'_> {
122    fn get(&self, key: &dyn crate::AsYaml) -> Option<YamlNode> {
123        crate::anchor_resolution::MergedMapping::get(self, key)
124    }
125
126    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (YamlNode, YamlNode)> + 'a> {
127        Box::new(crate::anchor_resolution::MergedMapping::iter(self))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::anchor_resolution::{
135        DocumentMergedExt, DocumentResolvedExt, MappingMergedExt, MergedMapping,
136    };
137    use crate::Document;
138    use std::str::FromStr;
139
140    /// A function generic over `impl MappingView` — proves the trait is
141    /// usable as an abstraction over both `Mapping` and `MergedMapping`.
142    fn collect_keys<M: MappingView + ?Sized>(view: &M) -> Vec<String> {
143        view.keys()
144            .map(|k| k.as_scalar().map(|s| s.as_string()).unwrap_or_default())
145            .collect()
146    }
147
148    fn doc(text: &str) -> Document {
149        Document::from_str(text).expect("parse")
150    }
151
152    #[test]
153    fn generic_works_on_plain_mapping() {
154        let d = doc("a: 1\nb: 2\nc: 3\n");
155        let root = d.as_mapping().unwrap();
156        assert_eq!(collect_keys(&root), vec!["a", "b", "c"]);
157        assert_eq!(<crate::yaml::Mapping as MappingView>::len(&root), 3);
158    }
159
160    #[test]
161    fn generic_works_on_merged_mapping() {
162        let yaml = "\
163d: &d
164  a: 1
165  b: 2
166m:
167  <<: *d
168  c: 3
169";
170        let d = doc(yaml);
171        let reg = d.build_anchor_registry();
172        let m = d.as_mapping().unwrap().get_mapping("m").unwrap();
173        let merged = m.merged(&reg);
174
175        // Direct + merged keys, in insertion-then-merge-source order.
176        assert_eq!(collect_keys(&merged), vec!["c", "a", "b"]);
177        assert_eq!(<MergedMapping as MappingView>::len(&merged), 3);
178    }
179
180    #[test]
181    fn trait_get_and_contains() {
182        let yaml = "\
183d: &d
184  x: 1
185m:
186  <<: *d
187  y: 2
188";
189        let d = doc(yaml);
190        let reg = d.build_anchor_registry();
191        let m = d.as_mapping().unwrap().get_mapping("m").unwrap();
192        let merged = m.merged(&reg);
193
194        let view: &dyn MappingView = &merged;
195        assert!(view.contains_key(&"x"));
196        assert!(view.contains_key(&"y"));
197        assert!(!view.contains_key(&"z"));
198        assert_eq!(view.get(&"x").unwrap().to_i64(), Some(1));
199    }
200
201    #[test]
202    fn trait_object_safety() {
203        // Compile-time check: building `&dyn MappingView` works.
204        let d = doc("a: 1\n");
205        let root = d.as_mapping().unwrap();
206        let view: &dyn MappingView = &root;
207        assert_eq!(view.len(), 1);
208    }
209
210    #[test]
211    fn is_empty_via_trait() {
212        let d = doc("{}\n");
213        let root = d.as_mapping().unwrap();
214        let view: &dyn MappingView = &root;
215        assert!(view.is_empty());
216    }
217
218    #[test]
219    fn values_iter_via_trait() {
220        let d = doc("a: 1\nb: 2\n");
221        let root = d.as_mapping().unwrap();
222        let view: &dyn MappingView = &root;
223        let nums: Vec<i64> = view.values().filter_map(|v| v.to_i64()).collect();
224        assert_eq!(nums, vec![1, 2]);
225    }
226
227    #[test]
228    fn merged_view_via_document_extension() {
229        // Round-trip the new trait through the owned MergedView container.
230        let yaml = "\
231d: &d
232  k: 42
233m:
234  <<: *d
235";
236        let d = doc(yaml);
237        let owned = d.merged().unwrap();
238        let m = owned.as_mapping().get_merged("m").unwrap();
239        // Use the trait method so the `&dyn AsYaml` signature is exercised.
240        let view: &dyn MappingView = &m;
241        assert_eq!(view.get(&"k").unwrap().to_i64(), Some(42));
242    }
243}