yaml_hash/lib.rs
1/*!
2Improved YAML Hash
3
4If the YAML data you're working with is well-defined and you want to write the necessary types, you
5should use [`serde`] and [`serde_yaml`].
6
7Otherwise, [`yaml_rust2`] provides a foundation for working with varied YAML data or when you don't
8want to write the necessary types.
9
10This crate provides the [`YamlHash`] struct, which is a wrapper for [`yaml_rust2::yaml::Hash`], and
11supports some additional capabilities:
12
13* Convert from [`&str`] via `impl From<&str>`
14* Convert to [`String`] via `impl Display`
15* Get a value for a dotted key as a [`YamlHash`] or [`yaml_rust2::Yaml`] via
16 [`get`][`YamlHash::get`] and [`get_yaml`][`YamlHash::get_yaml`]; return the root hash if the key
17 is `""`.
18* Merge a [`YamlHash`] with another [`YamlHash`], YAML hash string, or YAML hash file to create a
19 new [`YamlHash`] via [`merge`][`YamlHash::merge`], [`merge_str`][`YamlHash::merge_str`], or
20 [`merge_file`][`YamlHash::merge_file`]
21
22[`serde`]: https://docs.rs/serde
23[`serde_yaml`]: https://docs.rs/serde_yaml
24*/
25
26//--------------------------------------------------------------------------------------------------
27
28use {
29 anyhow::{Result, anyhow},
30 std::path::Path,
31 yaml_rust2::{YamlEmitter, YamlLoader, yaml::Hash},
32};
33
34pub use yaml_rust2::Yaml;
35
36//--------------------------------------------------------------------------------------------------
37
38/**
39Improved YAML Hash
40
41* Convert from [`&str`] via `impl From<&str>`
42* Convert to [`String`] via `impl Display`
43* Get a value for a dotted key as a [`YamlHash`] or [`yaml_rust2::Yaml`] via
44 [`get`][`YamlHash::get`] and [`get_yaml`][`YamlHash::get_yaml`]
45* Merge a [`YamlHash`] with another [`YamlHash`], YAML hash string, or YAML hash file to create a
46 new [`YamlHash`] via [`merge`][`YamlHash::merge`], [`merge_str`][`YamlHash::merge_str`], or
47 [`merge_file`][`YamlHash::merge_file`]
48
49*/
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub struct YamlHash {
52 data: Hash,
53}
54
55impl YamlHash {
56 /// Create a new empty [`YamlHash`]
57 #[must_use]
58 pub fn new() -> YamlHash {
59 YamlHash::default()
60 }
61
62 /**
63 Merge this [`YamlHash`] with another [`YamlHash`] to create a new [`YamlHash`]
64
65 ```
66 use yaml_hash::YamlHash;
67
68 let hash = YamlHash::from("\
69 fruit:
70 apple: 1
71 banana: 2\
72 ");
73
74 let other = YamlHash::from("\
75 fruit:
76 cherry:
77 sweet: 1
78 tart: 2\
79 ");
80
81 assert_eq!(
82 hash.merge(&other).to_string(),
83 "\
84 fruit:
85 apple: 1
86 banana: 2
87 cherry:
88 sweet: 1
89 tart: 2\
90 ",
91 );
92 ```
93 */
94 #[must_use]
95 pub fn merge(&self, other: &YamlHash) -> YamlHash {
96 let mut r = self.clone();
97 r.data = merge(&r.data, &other.data);
98 r
99 }
100
101 /**
102 Merge this [`YamlHash`] with a YAML hash [`&str`] to create a new [`YamlHash`]
103
104 ```
105 use yaml_hash::YamlHash;
106
107 let hash = YamlHash::from("\
108 fruit:
109 apple: 1
110 banana: 2\
111 ");
112
113 let hash = hash.merge_str("\
114 fruit:
115 cherry:
116 sweet: 1
117 tart: 2\
118 ").unwrap();
119
120 assert_eq!(
121 hash.to_string(),
122 "\
123 fruit:
124 apple: 1
125 banana: 2
126 cherry:
127 sweet: 1
128 tart: 2\
129 ",
130 );
131 ```
132
133 # Errors
134
135 Returns an error if the YAML string is not a hash
136 */
137 pub fn merge_str(&self, s: &str) -> Result<YamlHash> {
138 let mut r = self.clone();
139
140 for doc in YamlLoader::load_from_str(s)? {
141 if let Yaml::Hash(h) = doc {
142 r.data = merge(&r.data, &h);
143 } else {
144 return Err(anyhow!("YAML string is not a hash: {doc:?}"));
145 }
146 }
147
148 Ok(r)
149 }
150
151 /**
152 Merge this [`YamlHash`] with a YAML hash file to create a new [`YamlHash`]
153
154 ```
155 use yaml_hash::YamlHash;
156
157 let hash = YamlHash::from("\
158 fruit:
159 apple: 1
160 banana: 2\
161 ");
162
163 let hash = hash.merge_file("tests/b.yaml").unwrap();
164
165 assert_eq!(
166 hash.to_string(),
167 "\
168 fruit:
169 apple: 1
170 banana: 2
171 cherry: 3\
172 ",
173 );
174 ```
175
176 # Errors
177
178 Returns an error if not able to read the file at the given path to a string
179 */
180 pub fn merge_file<P: AsRef<Path>>(&self, path: P) -> Result<YamlHash> {
181 let yaml = std::fs::read_to_string(path)?;
182 self.merge_str(&yaml)
183 }
184
185 /**
186 Get the value for a dotted key as a [`Yaml`]
187
188 ```
189 use yaml_hash::{Yaml, YamlHash};
190
191 let hash = YamlHash::from("\
192 fruit:
193 apple: 1
194 banana: 2
195 cherry:
196 sweet: 1
197 tart: 2\
198 ");
199
200 assert_eq!(
201 hash.get_yaml("fruit.cherry.tart").unwrap(),
202 Yaml::Integer(2),
203 );
204 ```
205
206 # Errors
207
208 Returns an error if the given key is not valid or the value is not a hash
209 */
210 pub fn get_yaml(&self, key: &str) -> Result<Yaml> {
211 get_yaml(key, ".", &Yaml::Hash(self.data.clone()), "")
212 }
213
214 /**
215 Get a value for a dotted key as a [`YamlHash`]
216
217 ```
218 use yaml_hash::YamlHash;
219
220 let hash = YamlHash::from("\
221 fruit:
222 apple: 1
223 banana: 2
224 cherry:
225 sweet: 1
226 tart: 2\
227 ");
228
229 assert_eq!(
230 hash.get("fruit.cherry").unwrap(),
231 YamlHash::from("\
232 sweet: 1
233 tart: 2\
234 "),
235 );
236 ```
237
238 # Errors
239
240 Returns an error if the given key is not valid or the value is not a hash
241 */
242 pub fn get(&self, key: &str) -> Result<YamlHash> {
243 match self.get_yaml(key)?.into_hash() {
244 Some(data) => Ok(YamlHash { data }),
245 None => Err(anyhow!("Value for {key:?} is not a hash")),
246 }
247 }
248}
249
250impl std::fmt::Display for YamlHash {
251 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
252 let mut r = String::new();
253 let mut emitter = YamlEmitter::new(&mut r);
254 emitter.dump(&Yaml::Hash(self.data.clone())).unwrap();
255 r.replace_range(..4, ""); // remove "---\n" at beginning
256 write!(f, "{r}")
257 }
258}
259
260impl From<&str> for YamlHash {
261 /// Create a [`YamlHash`] from a YAML hash string
262 fn from(s: &str) -> YamlHash {
263 YamlHash::default().merge_str(s).unwrap()
264 }
265}
266
267//--------------------------------------------------------------------------------------------------
268
269fn merge(a: &Hash, b: &Hash) -> Hash {
270 let mut r = a.clone();
271 for (k, v) in b {
272 if let Yaml::Hash(bh) = v
273 && let Some(Yaml::Hash(rh)) = r.get(k)
274 {
275 if r.contains_key(k) {
276 r.replace(k.clone(), Yaml::Hash(merge(rh, bh)));
277 } else {
278 r.insert(k.clone(), Yaml::Hash(merge(rh, bh)));
279 }
280 continue;
281 }
282 if r.contains_key(k) {
283 r.replace(k.clone(), v.clone());
284 } else {
285 r.insert(k.clone(), v.clone());
286 }
287 }
288 r
289}
290
291fn get_yaml(key: &str, sep: &str, yaml: &Yaml, full: &str) -> Result<Yaml> {
292 if key.is_empty() {
293 return Ok(yaml.clone());
294 }
295
296 let mut s = key.split(sep);
297 let this = s.next().unwrap();
298 let next = s.collect::<Vec<&str>>().join(sep);
299
300 match yaml {
301 Yaml::Hash(hash) => match hash.get(&Yaml::String(this.to_string())) {
302 Some(v) => {
303 if next.is_empty() {
304 Ok(v.clone())
305 } else {
306 let full = if full.is_empty() {
307 key.to_string()
308 } else {
309 format!("{full}.{this}")
310 };
311 get_yaml(&next, sep, v, &full)
312 }
313 }
314 None => Err(anyhow!("Invalid key: {full:?}")),
315 },
316 _ => Err(anyhow!("Value for key {full:?} is not a hash")),
317 }
318}