use {
anyhow::{Result, anyhow},
std::path::Path,
yaml_rust2::{YamlEmitter, YamlLoader, yaml::Hash},
};
pub use yaml_rust2::Yaml;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct YamlHash {
data: Hash,
}
impl YamlHash {
#[must_use]
pub fn new() -> YamlHash {
YamlHash::default()
}
#[must_use]
pub fn merge(&self, other: &YamlHash) -> YamlHash {
let mut r = self.clone();
r.data = merge(&r.data, &other.data);
r
}
pub fn merge_str(&self, s: &str) -> Result<YamlHash> {
let mut r = self.clone();
for doc in YamlLoader::load_from_str(s)? {
if let Yaml::Hash(h) = doc {
r.data = merge(&r.data, &h);
} else {
return Err(anyhow!("YAML string is not a hash: {doc:?}"));
}
}
Ok(r)
}
pub fn merge_file<P: AsRef<Path>>(&self, path: P) -> Result<YamlHash> {
let yaml = std::fs::read_to_string(path)?;
self.merge_str(&yaml)
}
pub fn get_yaml(&self, key: &str) -> Result<Yaml> {
get_yaml(key, ".", &Yaml::Hash(self.data.clone()), "")
}
pub fn get(&self, key: &str) -> Result<YamlHash> {
match self.get_yaml(key)?.into_hash() {
Some(data) => Ok(YamlHash { data }),
None => Err(anyhow!("Value for {key:?} is not a hash")),
}
}
}
impl std::fmt::Display for YamlHash {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut r = String::new();
let mut emitter = YamlEmitter::new(&mut r);
emitter.dump(&Yaml::Hash(self.data.clone())).unwrap();
r.replace_range(..4, ""); write!(f, "{r}")
}
}
impl From<&str> for YamlHash {
fn from(s: &str) -> YamlHash {
YamlHash::default().merge_str(s).unwrap()
}
}
fn merge(a: &Hash, b: &Hash) -> Hash {
let mut r = a.clone();
for (k, v) in b {
if let Yaml::Hash(bh) = v
&& let Some(Yaml::Hash(rh)) = r.get(k)
{
if r.contains_key(k) {
r.replace(k.clone(), Yaml::Hash(merge(rh, bh)));
} else {
r.insert(k.clone(), Yaml::Hash(merge(rh, bh)));
}
continue;
}
if r.contains_key(k) {
r.replace(k.clone(), v.clone());
} else {
r.insert(k.clone(), v.clone());
}
}
r
}
fn get_yaml(key: &str, sep: &str, yaml: &Yaml, full: &str) -> Result<Yaml> {
if key.is_empty() {
return Ok(yaml.clone());
}
let mut s = key.split(sep);
let this = s.next().unwrap();
let next = s.collect::<Vec<&str>>().join(sep);
match yaml {
Yaml::Hash(hash) => match hash.get(&Yaml::String(this.to_string())) {
Some(v) => {
if next.is_empty() {
Ok(v.clone())
} else {
let full = if full.is_empty() {
key.to_string()
} else {
format!("{full}.{this}")
};
get_yaml(&next, sep, v, &full)
}
}
None => Err(anyhow!("Invalid key: {full:?}")),
},
_ => Err(anyhow!("Value for key {full:?} is not a hash")),
}
}