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 toml_rs::{self as toml, Value};

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

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

impl UniNodeFormat for TomlFormat {
    fn extensions(&self) -> &[&str] {
        &["toml"]
    }

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

        fn convert(node: Value) -> Result<UniNode, UniNodeFmtError> {
            match node {
                Value::Boolean(v) => Ok(UniNode::Boolean(v)),
                Value::Integer(v) => Ok(UniNode::Integer(v)),
                Value::Float(v) => Ok(UniNode::Float(v)),
                Value::String(v) => Ok(UniNode::String(v)),
                Value::Array(v) => {
                    let mut data = Array::with_capacity(v.len());
                    for node in v {
                        data.push(convert(node)?);
                    }
                    Ok(UniNode::Array(data))
                },
                Value::Table(v) => {
                    let mut data = Object::new();
                    for (key, node) in v {
                        data.insert(key, convert(node)?);
                    }
                    Ok(UniNode::Object(data))
                },
                Value::Datetime(v) => Ok(UniNode::String(v.to_string())),
            }
        }

        convert(root)
    }

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

        let value = convert(node)?;
        toml::to_string(&value).map_err(|e| UniNodeFmtError::Format(e.into()))
    }
}

pub fn parse_toml<S>(text: S) -> Result<UniNode, UniNodeFmtError>
where
    S: AsRef<str>,
{
    TomlFormat.parse(text.as_ref())
}

pub fn compile_toml(node: &UniNode) -> Result<String, UniNodeFmtError> {
    TomlFormat.build(node)
}

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

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

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

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