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}