uninode 0.4.2

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

use super::node::{Map, UniNode};

const PATH_DELIMITER: char = '.';

macro_rules! impl_find_function {
    ($name:ident, $conv:ident, $type:ty) => {
        pub fn $name(&self, path: &str) -> Option<$type> {
            self.find(path).and_then(|v| v.$conv().ok())
        }
    };
}

impl UniNode {
    impl_find_function!(find_bool, as_bool, bool);

    impl_find_function!(find_int, as_int, i64);

    impl_find_function!(find_uint, as_uint, u64);

    impl_find_function!(find_float, as_float, f64);

    impl_find_function!(find_str, as_str, &str);

    impl_find_function!(find_bytes, as_bytes, &[u8]);

    pub fn find_array(&self, path: &str) -> Option<&Vec<UniNode>> {
        self.find(path).and_then(|v| v.as_array().ok())
    }

    pub fn find_object(&self, path: &str) -> Option<&Map<String, UniNode>> {
        self.find(path).and_then(|v| v.as_object().ok())
    }

    pub fn find(&self, path: &str) -> Option<&Self> {
        let names = path.split(PATH_DELIMITER);

        fn find_path<'a, 'b, I>(
            root: &'a UniNode, mut names: I,
        ) -> Option<&'a UniNode>
        where
            I: Iterator<Item = &'b str>,
        {
            if let Some(name) = names.next() {
                if !name.is_empty() {
                    if let UniNode::Object(tbl) = root {
                        if let Some(chd) = tbl.get(name) {
                            return find_path(chd, names);
                        }
                    }
                    return None;
                }
            };
            Some(root)
        }

        find_path(self, names)
    }

    pub fn find_mut(&mut self, path: &str) -> Option<&mut Self> {
        let names = path.split(PATH_DELIMITER);

        fn find_path<'a, 'b, I>(
            root: &'a mut UniNode, mut names: I,
        ) -> Option<&'a mut UniNode>
        where
            I: Iterator<Item = &'b str>,
        {
            if let Some(name) = names.next() {
                if !name.is_empty() {
                    if let UniNode::Object(tbl) = root {
                        if let Some(chd) = tbl.get_mut(name) {
                            return find_path(chd, names);
                        }
                    }
                    return None;
                }
            };
            Some(root)
        }

        find_path(self, names)
    }

    pub fn merge(&mut self, other: UniNode) {
        fn merge_node(dst: &mut UniNode, src: UniNode) {
            match dst {
                UniNode::Null => *dst = src,
                UniNode::Array(a) => {
                    if src.is_array() {
                        if let UniNode::Array(src) = src {
                            a.extend(src);
                        }
                    } else {
                        a.push(src);
                    }
                },
                UniNode::Object(o) if src.is_object() => {
                    if let UniNode::Object(src) = src {
                        #[allow(clippy::map_entry)]
                        for (key, node) in src {
                            if o.contains_key(&key) {
                                merge_node(o.get_mut(&key).unwrap(), node);
                            } else {
                                o.insert(key, node);
                            }
                        }
                    }
                },
                _ => {
                    if !src.is_null() {
                        let v = match src {
                            a @ UniNode::Array(_) => a,
                            a => UniNode::Array(vec![a]),
                        };
                        let v = std::mem::replace(dst, v);
                        dst.as_array_mut().unwrap().insert(0, v);
                    }
                },
            }
        }

        merge_node(self, other)
    }
}

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

    #[test]
    fn find() {
        let mut val = unode!(
            "server" => unode!("host" => "localhost", "port" => 999),
            "two" => 2);

        assert!(val.find("tw").is_none());
        assert!(val.find("server.url").is_none());
        assert!(val.find("server.").is_some());
        assert!(val.find("server.").unwrap().is_object());
        assert!(val.find("server.host").is_some());
        assert!(val.find("server.host").unwrap().is_string());
        assert_eq!(
            val.find("server.host").unwrap().as_str().unwrap(),
            "localhost"
        );

        assert!(val.find("server.timeout").is_none());
        let n = val.find_mut("server").unwrap().as_object_mut().unwrap();
        n.insert("timeout".to_string(), UniNode::Integer(10));

        assert!(val.find("server.timeout").is_some());
        assert_eq!(
            val.find("server.timeout")
                .and_then(|v| v.as_int().ok())
                .unwrap(),
            10
        );
    }

    #[test]
    fn get() {
        let val = unode!(
            "server" => unode!(
                "host" => "localhost",
                "port" => 999,
                "keys" => unode!(1, 2, 4, 7),
            ),
            "bool" => true,
            "int" => 33,
            "uint" => 12u32,
            "float" => 1.2,
            "str" => "hello",
            "bytes" => vec![1u8,2u8,3u8],
            "opt" => Some("opt"),
            "none" => Option::<u8>::None,
            "arr" => unode!["one", 2, 3.3],
        );

        assert!(val.find_bool("bool").unwrap());
        assert_eq!(val.find_int("int").unwrap(), 33);
        assert_eq!(val.find_uint("uint").unwrap(), 12);
        assert!((val.find_float("float").unwrap() - 1.2).abs() < f64::EPSILON);
        assert_eq!(val.find_str("str").unwrap(), "hello");
        assert_eq!(val.find_bytes("bytes").unwrap(), vec![1, 2, 3]);
        assert_eq!(val.find_str("opt").unwrap(), "opt");
        assert!(val.find("none").unwrap().is_null());

        let keys = val
            .find_array("server.keys")
            .unwrap()
            .iter()
            .map(|v| v.as_int().unwrap())
            .collect::<Vec<_>>();
        assert_eq!(keys, vec![1, 2, 4, 7]);
    }

    #[test]
    fn merge() {
        fn test_merge(mut root: UniNode, other: UniNode, res: UniNode) {
            root.merge(other);
            assert_eq!(root, res);
        }
        test_merge(unode!(), unode!(), unode!());
        test_merge(unode!(), unode![1, 2], unode![1, 2]);
        test_merge(unode!(1), unode!(2), unode![1, 2]);
        test_merge(unode!(1), unode![2, 3], unode![1, 2, 3]);
        test_merge(unode!(1), unode! {"1" => 1}, unode![1, unode! {"1" => 1}]);
        test_merge(unode![1, 2], unode![3, 4], unode![1, 2, 3, 4]);
        test_merge(unode![1, 2], unode!(3), unode![1, 2, 3]);
        test_merge(
            unode![1, 2],
            unode! {"3" => 3},
            unode![1, 2, unode! {"3" => 3}],
        );
        test_merge(unode! {"1" => 1}, unode!(2), unode![unode! {"1" => 1}, 2]);
        test_merge(
            unode! {"1" => 1},
            unode![2, 3],
            unode![unode! {"1" => 1}, 2, 3],
        );
        test_merge(
            unode! {"1" => 1},
            unode! {"2" => 2},
            unode! {"1" => 1, "2" => 2},
        );
        test_merge(
            unode! {"zero" => unode!(), "one" => 1, "two" => unode!{"right" => 2}},
            unode! {"zero" => 0, "one" => 1.1, "two" => unode!{"left" => 1}, "three" => 3},
            unode! {
                "zero" => 0,
                "one" => unode![1, 1.1],
                "two" => unode!{"right" => 2, "left" => 1},
                "three" => 3
            },
        );
    }
}