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. |
43 | fn merge_hashes(mut hash: Hash, rhs: Hash) -> Hash { |
44 | rhs.into_iter().for_each(|(key: Yaml, value: Yaml)| { |
45 | hash.entry(key).or_insert(default:value); |
46 | }); |
47 | hash |
48 | } |
49 | |
50 | /// Merge values together. |
51 | fn merge_values(hash: Hash, value: Yaml) -> Result<Hash, MergeKeyError> { |
52 | let merge_values: LinkedHashMap = match value { |
53 | Yaml::Array(arr: Vec) => { |
54 | let init: Result<Hash, _> = Ok(Hash::new()); |
55 | |
56 | arr.into_iter().fold(init, |res_hash: Result, …>, item: Yaml| { |
57 | // Merge in the next item. |
58 | res_hash.and_then(op:move |res_hash: LinkedHashMap| { |
59 | if let Yaml::Hash(next_hash: LinkedHashMap) = item { |
60 | Ok(merge_hashes(res_hash, rhs: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: LinkedHashMap) => merge_hash, |
69 | _ => return Err(MergeKeyError::InvalidMergeValue), |
70 | }; |
71 | |
72 | Ok(merge_hashes(hash, rhs:merge_values)) |
73 | } |
74 | |
75 | /// Recurse into a hash and handle items with merge keys in them. |
76 | fn merge_hash(hash: Hash) -> Result<Yaml, MergeKeyError> { |
77 | let mut hash: LinkedHashMap = hashimpl Iterator- >
|
78 | .into_iter() |
79 | // First handle any merge keys in the key or value... |
80 | .map(|(key: Yaml, value: Yaml)| { |
81 | merge_keys(key).and_then(|key: Yaml| merge_keys(value).map(|value: Yaml| (key, value))) |
82 | }) |
83 | .collect::<Result<Hash, _>>()?; |
84 | |
85 | if let Some(merge_value: Yaml) = hash.remove(&MERGE_KEY) { |
86 | merge_values(hash, merge_value).map(op: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. |
93 | fn merge_array(arr: Array) -> Result<Yaml, MergeKeyError> { |
94 | arr.into_iter() |
95 | .map(merge_keys) |
96 | .collect::<Result<Array, _>>() |
97 | .map(op:Yaml::Array) |
98 | } |
99 | |
100 | /// Handle merge keys in a YAML document. |
101 | pub fn merge_keys(doc: Yaml) -> Result<Yaml, MergeKeyError> { |
102 | match doc { |
103 | Yaml::Hash(hash: LinkedHashMap) => merge_hash(hash), |
104 | Yaml::Array(arr: Vec) => merge_array(arr), |
105 | _ => Ok(doc), |
106 | } |
107 | } |
108 | |