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
7use crates::thiserror::Error;
8use crates::yaml_rust::yaml::{Array, Hash};
9use 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]
17pub 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
37lazy_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.
43fn 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.
51fn 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.
76fn 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.
93fn 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.
101pub 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