1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#![doc = include_str!("../README.md")]

use linked_hash_map::Entry;
use yaml_rust::yaml::Hash;
pub use yaml_rust::Yaml;
use yaml_rust::{YamlEmitter, YamlLoader};

#[cfg(test)]
mod tests;

/**

YAML Hash with merge/update capabilities

Wrapper around `yaml_rust::yaml::Hash`, which is a type alias for `linked_hash_map::LinkedHashMap`

*/
#[derive(Debug, Default)]
pub struct MergeYamlHash {
    pub data: Hash,
}

impl MergeYamlHash {
    /**

    Create a new/empty hash

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    assert_eq!(format!("{:?}", hash), "MergeYamlHash { data: {} }");
    assert_eq!(format!("{:#?}", hash), "MergeYamlHash {\n    data: {},\n}");
    ```

    */
    pub fn new() -> MergeYamlHash {
        MergeYamlHash::default()
    }

    /**

    Merge YAML file or string

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    hash.merge("tests/a.yaml");
    assert_eq!(hash.to_string(), "apple: 1\nbanana: 2");
    ```

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    let yaml = "apple: 1\nbanana: 2".to_string();
    hash.merge(&yaml);
    assert_eq!(hash.to_string(), yaml);
    ```

    */
    pub fn merge(&mut self, file_or_str: &str) {
        let path = std::path::Path::new(&file_or_str);
        let yaml = if path.is_file() {
            std::fs::read_to_string(path).unwrap()
        } else {
            file_or_str.to_string()
        };
        for doc in YamlLoader::load_from_str(&yaml).unwrap() {
            if let Yaml::Hash(h) = doc {
                self.data = merge_hashes(&self.data, &h);
            }
        }
    }

    /**

    Merge multiple YAML files or strings in order

    No conflicts:

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    let yaml1 = "apple: 1\nbanana: 2".to_string();
    let yaml2 = "cherry: 3".to_string();
    let result = "apple: 1\nbanana: 2\ncherry: 3";
    hash.merge_vec(vec![yaml1, yaml2]);
    assert_eq!(hash.to_string(), result);
    ```

    With conflict; value in `yaml2.banana` simply overrides the value in
    `yaml1.banana`:

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    let yaml1 = "apple: 1\nbanana: 2".to_string();
    let yaml2 = "banana: 3".to_string();
    let result = "apple: 1\nbanana: 3";
    hash.merge_vec(vec![yaml1, yaml2]);
    assert_eq!(hash.to_string(), result);
    ```

    */
    pub fn merge_vec(&mut self, files_or_strings: Vec<String>) {
        for file_or_string in files_or_strings {
            self.merge(&file_or_string);
        }
    }
}

impl std::fmt::Display for MergeYamlHash {
    /**

    Serialize to YAML string

    ```
    use merge_yaml_hash::MergeYamlHash;
    let mut hash = MergeYamlHash::new();
    let yaml = "apple: 1\nbanana: 2".to_string();
    hash.merge(&yaml);
    assert_eq!(hash.to_string(), yaml);
    ```

    */
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let mut r = String::new();
        let mut emitter = YamlEmitter::new(&mut r);
        let yaml = Yaml::Hash(self.data.clone());
        emitter.dump(&yaml).unwrap();
        r.replace_range(..4, ""); // remove "---\n" at beginning
        write!(f, "{r}")
    }
}

/**

Merge two YAML hashes

*/
fn merge_hashes(a: &Hash, b: &Hash) -> Hash {
    let mut r = a.clone();
    for (k, v) in b.iter() {
        if let Yaml::Hash(bh) = v {
            if let Entry::Occupied(e) = r.entry(k.clone()) {
                if let Yaml::Hash(rh) = e.get().clone() {
                    r.entry(k.clone())
                        .and_modify(|e| *e = Yaml::Hash(merge_hashes(&rh, bh)))
                        .or_insert_with(|| Yaml::Hash(merge_hashes(&rh, bh)));
                    continue;
                }
            }
        }
        r.entry(k.clone())
            .and_modify(|e| *e = v.clone())
            .or_insert_with(|| v.clone());
    }
    r
}