docker_compose_spec 0.2.1

docker-compose.yml <-> rust <-> serde_json
Documentation
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"),
        }
    }
}

/// yaml-rust anchors `&val; <<: *val`  resolver
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)
}