use std::sync::LazyLock;
use anyhow::bail;
use serde::de::DeserializeOwned;
use serde_yaml::{
value::{Tag, TaggedValue},
Value,
};
use yaml_rust::{
yaml::{Array, Hash},
Yaml,
};
const TAG: &str = "7e0c2a23-0da6-4c3a-84bb-40134e9fc55e";
const VALUE: &str = "8ed87015-da61-4a2f-8f19-cd06cb9b77e4";
static MERGE: LazyLock<Yaml> = LazyLock::new(|| Yaml::String("<<".into()));
struct Wrapper(Yaml);
impl Wrapper {
fn yaml(self) -> Yaml {
self.0
}
}
fn merge_hashes(hash: Hash, merged_hash: Hash) -> Hash {
merged_hash.into_iter().fold(hash, |mut z, (k, v)| {
z.entry(k).or_insert(v);
z
})
}
fn merge_values(hash: Hash, value: Yaml) -> anyhow::Result<Hash> {
let merged_vals = match value {
Yaml::Array(arr) => {
let accum: Result<Hash, _> = Ok(Hash::new());
arr.into_iter().fold(accum, |z, item| {
z.and_then(move |res_hash| {
if let Yaml::Hash(next_hash) = item {
Ok(merge_hashes(res_hash, next_hash))
} else {
bail!("values without hash are not allowed here")
}
})
})?
}
Yaml::Hash(merged_hash) => merged_hash,
_ => bail!("only mappings and sequences can be merged"),
};
Ok(merge_hashes(hash, merged_vals))
}
fn merge_hash(hash: Hash) -> anyhow::Result<Yaml> {
let mut hash = hash
.into_iter()
.map(|(key, value)| {
resolve_yaml(key).and_then(|key| resolve_yaml(value).map(|value| (key, value)))
})
.collect::<Result<Hash, _>>()?;
if let Some(merge_value) = hash.remove(&MERGE) {
merge_values(hash, merge_value).map(Yaml::Hash)
} else {
Ok(Yaml::Hash(hash))
}
}
fn merge_array(arr: Array) -> anyhow::Result<Yaml> {
arr.into_iter().map(resolve_yaml).collect::<Result<Array, _>>().map(Yaml::Array)
}
fn as_tagged_value(mut hash: Hash) -> Result<(Tag, Value), Hash> {
let value_key = Yaml::String(VALUE.into());
let tag_key = Yaml::String(TAG.into());
let tags = hash.len() == 2 && hash.contains_key(&value_key);
if tags {
let Some(Yaml::String(tag)) = hash.remove(&tag_key) else {
return Err(hash);
};
let Some(value) = hash.remove(&value_key) else {
return Err(hash);
};
Ok((Tag::new(tag), Wrapper(value).into()))
} else {
Err(hash)
}
}
impl From<Value> for Wrapper {
fn from(yaml: Value) -> Self {
Wrapper(match yaml {
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Yaml::Integer(i)
} else {
Yaml::Real(n.to_string())
}
}
Value::String(s) => Yaml::String(s),
Value::Bool(s) => Yaml::Boolean(s),
Value::Sequence(seq) => {
Yaml::Array(seq.into_iter().map(Into::into).map(Self::yaml).collect())
}
Value::Mapping(map) => Yaml::Hash(
map.into_iter()
.map(|(k, v)| (Self::yaml(k.into()), Self::yaml(v.into())))
.collect(),
),
Value::Tagged(tagged) => Yaml::Hash(
[
(Yaml::String(TAG.into()), Yaml::String(tagged.tag.to_string())),
(Yaml::String(VALUE.into()), Self::from(tagged.value).0),
]
.into_iter()
.collect(),
),
Value::Null => Yaml::Null,
})
}
}
impl From<Wrapper> for Value {
fn from(yaml: Wrapper) -> Self {
match yaml.0 {
Yaml::Real(f) => match serde_yaml::from_str(&f) {
Ok(f) => Value::Number(f),
Err(_) => Value::String(f),
},
Yaml::Integer(i) => Value::Number(i.into()),
Yaml::String(s) => Value::String(s),
Yaml::Boolean(b) => Value::Bool(b),
Yaml::Array(array) => {
Value::Sequence(array.into_iter().map(|item| Wrapper(item).into()).collect())
}
Yaml::Hash(hash) => match as_tagged_value(hash) {
Ok((tag, value)) => Value::Tagged(Box::new(TaggedValue { tag, value })),
Err(hash) => Value::Mapping(
hash.into_iter().map(|(k, v)| (Wrapper(k).into(), Wrapper(v).into())).collect(),
),
},
Yaml::Null => Value::Null,
Yaml::Alias(_) => unreachable!("Yaml.Alias not implemented"),
Yaml::BadValue => unreachable!("Yaml.BadValue"),
}
}
}
pub fn resolve_yaml(doc: Yaml) -> anyhow::Result<Yaml> {
match doc {
Yaml::Hash(hash) => merge_hash(hash),
Yaml::Array(arr) => merge_array(arr),
_ => Ok(doc),
}
}
fn resolve(doc: Wrapper) -> anyhow::Result<Wrapper> {
resolve_yaml(doc.0).map(Wrapper)
}
pub fn from_value<T: DeserializeOwned>(doc: Value) -> anyhow::Result<T> {
resolve(doc.into()).and_then(|v| serde_yaml::from_value(v.into()).map_err(Into::into))
}
pub fn from_slice<T: DeserializeOwned>(bytes: &[u8]) -> anyhow::Result<T> {
serde_yaml::from_slice(bytes).map_err(Into::into).and_then(from_value)
}
#[allow(unused)]
pub fn from_str<T: DeserializeOwned>(yaml: &str) -> anyhow::Result<T> {
serde_yaml::from_str(yaml).map_err(Into::into).and_then(from_value)
}