uninode 0.4.4

Universal object type
Documentation
//
// Copyright (c) 2022 Oleg Lelenkov <o.lelenkov@gmail.com>
// Distributed under terms of the BSD 3-Clause license.
//

use anyhow::anyhow;
use yaml_rust::{yaml, Yaml, YamlLoader, YamlEmitter};

use crate::value::{Array, Object, UniNode, Expected};
use super::{UniNodeFormat, UniNodeFmtError};

#[derive(Clone, Copy, Default)]
pub struct YamlFormat;

impl UniNodeFormat for YamlFormat {
    fn extensions(&self) -> &[&str] {
        &["yaml", "yml"]
    }

    fn parse(&self, text: &str) -> Result<UniNode, UniNodeFmtError> {
        let mut root = YamlLoader::load_from_str(text)
            .map_err(|e| UniNodeFmtError::Format(e.into()))?;

        fn convert(node: Yaml) -> Result<UniNode, UniNodeFmtError> {
            match node {
                Yaml::Null => Ok(UniNode::Null),
                Yaml::Boolean(val) => Ok(UniNode::Boolean(val)),
                Yaml::Integer(val) => Ok(UniNode::Integer(val)),
                Yaml::String(val) => Ok(UniNode::String(val)),
                Yaml::Real(val) => Ok(UniNode::Float(val.parse::<f64>()?)),
                Yaml::Array(val) => {
                    let mut data = Array::with_capacity(val.len());
                    for node in val {
                        data.push(convert(node)?);
                    }
                    Ok(UniNode::Array(data))
                },
                Yaml::Hash(val) => {
                    let mut data = Object::new();
                    for (key, node) in val {
                        let key = key.as_str().ok_or_else(|| {
                            UniNodeFmtError::Conv("Non-string key".to_string())
                        })?;
                        data.insert(key.to_string(), convert(node)?);
                    }
                    Ok(UniNode::Object(data))
                },
                Yaml::Alias(_) => Err(UniNodeFmtError::Conv(
                    "Yaml alias, not fully supported yet".to_string(),
                )),
                _ => {
                    Err(UniNodeFmtError::Conv("Yaml invalid type".to_string()))
                },
            }
        }

        let root = match root.len() {
            0 => Yaml::Hash(yaml::Hash::new()),
            1 => std::mem::replace(&mut root[0], Yaml::Null),
            n => {
                return Err(UniNodeFmtError::Format(anyhow!(
                    "Got {} YAML documents, expected 1",
                    n
                )))
            },
        };

        convert(root)
    }

    fn build(&self, node: &UniNode) -> Result<String, UniNodeFmtError> {
        fn convert(node: &UniNode) -> Result<Yaml, UniNodeFmtError> {
            match node {
                UniNode::Null => Ok(Yaml::Null),
                UniNode::Boolean(v) => Ok(Yaml::Boolean(*v)),
                UniNode::Integer(v) => Ok(Yaml::Integer(*v)),
                UniNode::UInteger(v) => Ok(Yaml::Integer(i64::try_from(*v)?)),
                UniNode::Float(v) => Ok(Yaml::Real(v.to_string())),
                UniNode::String(v) => Ok(Yaml::String(v.clone())),
                UniNode::Bytes(_) => {
                    Err(UniNodeFmtError::NotSupportedType(Expected::Bytes))
                },
                UniNode::Array(v) => {
                    let mut array =
                        yaml_rust::yaml::Array::with_capacity(v.len());
                    for sub in v {
                        array.push(convert(sub)?);
                    }
                    Ok(Yaml::Array(array))
                },
                UniNode::Object(v) => {
                    let mut table =
                        yaml_rust::yaml::Hash::with_capacity(v.len());
                    for (key, sub) in v {
                        let key = Yaml::String(key.clone());
                        table.insert(key, convert(sub)?);
                    }
                    Ok(Yaml::Hash(table))
                },
            }
        }

        let mut out = String::new();
        let mut emitter = YamlEmitter::new(&mut out);
        emitter
            .dump(&convert(node)?)
            .map_err(|e| UniNodeFmtError::Format(e.into()))?;
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn yaml_format() {
        let text = r#"
                client:
                    host: "localhost"
                    port: 404
                    can: 2001-12-15T02:59:43.1Z
                    collection:
                    - "one"
                    - "two"
            "#;

        let node = YamlFormat.parse(text).unwrap();
        assert_eq!(node.find_int("client.port").unwrap(), 404);
        assert_eq!(node.find_str("client.host").unwrap(), "localhost");

        let text = YamlFormat.build(&node).unwrap();
        let node = YamlFormat.parse(&text).unwrap();
        assert_eq!(node.find_int("client.port").unwrap(), 404);
        assert_eq!(node.find_str("client.host").unwrap(), "localhost");
    }
}