1 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4 // option. This file may not be copied, modified, or distributed
5 // except according to those terms.
6 
7 use crates::thiserror::Error;
8 use crates::yaml_rust::yaml::{Array, Hash};
9 use crates::yaml_rust::Yaml;
10 
11 /// Errors which may occur when performing the YAML merge key process.
12 ///
13 /// This enum is `non_exhaustive`, but cannot be marked as such until it is stable. In the
14 /// meantime, there is a hidden variant.
15 #[derive(Debug, Error)]
16 // TODO: #[non_exhaustive]
17 pub enum MergeKeyError {
18     /// A non-hash value was given as a value to merge into a hash.
19     ///
20     /// This happens with a document such as:
21     ///
22     /// ```yaml
23     /// -
24     ///   <<: 4
25     ///   x: 1
26     /// ```
27     #[error("only mappings and arrays of mappings may be merged")]
28     InvalidMergeValue,
29     /// This is here to force `_` matching right now.
30     ///
31     /// **DO NOT USE**
32     #[doc(hidden)]
33     #[error("unreachable...")]
34     _NonExhaustive,
35 }
36 
37 lazy_static! {
38     /// The name of the key to use for merge data.
39     static ref MERGE_KEY: Yaml = Yaml::String("<<".into());
40 }
41 
42 /// Merge two hashes together.
merge_hashes(mut hash: Hash, rhs: Hash) -> Hash43 fn merge_hashes(mut hash: Hash, rhs: Hash) -> Hash {
44     rhs.into_iter().for_each(|(key, value)| {
45         hash.entry(key).or_insert(value);
46     });
47     hash
48 }
49 
50 /// Merge values together.
merge_values(hash: Hash, value: Yaml) -> Result<Hash, MergeKeyError>51 fn merge_values(hash: Hash, value: Yaml) -> Result<Hash, MergeKeyError> {
52     let merge_values = match value {
53         Yaml::Array(arr) => {
54             let init: Result<Hash, _> = Ok(Hash::new());
55 
56             arr.into_iter().fold(init, |res_hash, item| {
57                 // Merge in the next item.
58                 res_hash.and_then(move |res_hash| {
59                     if let Yaml::Hash(next_hash) = item {
60                         Ok(merge_hashes(res_hash, next_hash))
61                     } else {
62                         // Non-hash values at this level are not allowed.
63                         Err(MergeKeyError::InvalidMergeValue)
64                     }
65                 })
66             })?
67         },
68         Yaml::Hash(merge_hash) => merge_hash,
69         _ => return Err(MergeKeyError::InvalidMergeValue),
70     };
71 
72     Ok(merge_hashes(hash, merge_values))
73 }
74 
75 /// Recurse into a hash and handle items with merge keys in them.
merge_hash(hash: Hash) -> Result<Yaml, MergeKeyError>76 fn merge_hash(hash: Hash) -> Result<Yaml, MergeKeyError> {
77     let mut hash = hash
78         .into_iter()
79         // First handle any merge keys in the key or value...
80         .map(|(key, value)| {
81             merge_keys(key).and_then(|key| merge_keys(value).map(|value| (key, value)))
82         })
83         .collect::<Result<Hash, _>>()?;
84 
85     if let Some(merge_value) = hash.remove(&MERGE_KEY) {
86         merge_values(hash, merge_value).map(Yaml::Hash)
87     } else {
88         Ok(Yaml::Hash(hash))
89     }
90 }
91 
92 /// Recurse into an array and handle items with merge keys in them.
merge_array(arr: Array) -> Result<Yaml, MergeKeyError>93 fn merge_array(arr: Array) -> Result<Yaml, MergeKeyError> {
94     arr.into_iter()
95         .map(merge_keys)
96         .collect::<Result<Array, _>>()
97         .map(Yaml::Array)
98 }
99 
100 /// Handle merge keys in a YAML document.
merge_keys(doc: Yaml) -> Result<Yaml, MergeKeyError>101 pub fn merge_keys(doc: Yaml) -> Result<Yaml, MergeKeyError> {
102     match doc {
103         Yaml::Hash(hash) => merge_hash(hash),
104         Yaml::Array(arr) => merge_array(arr),
105         _ => Ok(doc),
106     }
107 }
108