minecraft-command-types 0.1.0

Provides an AST like structure for Minecraft commands.
Documentation
use crate::snbt::{SNBT, fmt_snbt_compound};
use minecraft_command_types_derive::HasMacro;
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};

pub type SNBTCompound = BTreeMap<String, SNBT>;

fn escape_nbt_path_key(name: &str) -> String {
    let needs_quotes = name
        .chars()
        .any(|c| matches!(c, ' ' | '"' | '\'' | '[' | ']' | '.' | '{' | '}'));

    if needs_quotes {
        let escaped_content = name.replace('\\', "\\\\").replace('"', "\\\"");
        format!("\"{}\"", escaped_content)
    } else {
        name.to_string()
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
pub enum NbtPathNode {
    RootCompound(SNBTCompound),
    Named(String, Option<SNBTCompound>),
    Index(Option<SNBT>),
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
pub struct NbtPath(pub Vec<NbtPathNode>);

impl Display for NbtPathNode {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            NbtPathNode::RootCompound(compound) => fmt_snbt_compound(f, compound),
            NbtPathNode::Named(name, filter) => {
                write!(f, "{}", escape_nbt_path_key(name))?;

                if let Some(filter) = filter
                    && !filter.is_empty()
                {
                    fmt_snbt_compound(f, filter)?;
                }
                Ok(())
            }
            NbtPathNode::Index(Some(snbt)) => write!(f, "[{}]", snbt),
            NbtPathNode::Index(None) => write!(f, "[]"),
        }
    }
}

impl Display for NbtPath {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut first = true;
        for node in &self.0 {
            if !first && !matches!(node, NbtPathNode::Index(_)) {
                write!(f, ".")?;
            }
            first = false;
            write!(f, "{}", node)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::snbt::SNBT;
    use std::collections::BTreeMap;

    fn snbt_string(s: &str) -> SNBT {
        SNBT::String(s.to_string())
    }

    fn compound(pairs: Vec<(&str, SNBT)>) -> SNBTCompound {
        let mut map = BTreeMap::new();
        for (k, v) in pairs {
            map.insert(k.to_string(), v);
        }
        map
    }

    #[test]
    fn test_example_1() {
        let path = NbtPath(vec![
            NbtPathNode::Named("foo".to_string(), None),
            NbtPathNode::Named("bar".to_string(), None),
            NbtPathNode::Index(Some(SNBT::Integer(0))),
            NbtPathNode::Named("A [crazy name]!".to_string(), None),
            NbtPathNode::Named("baz".to_string(), None),
        ]);

        assert_eq!(path.to_string(), r#"foo.bar[0]."A [crazy name]!".baz"#);
    }

    #[test]
    fn test_example_2() {
        let path = NbtPath(vec![
            NbtPathNode::Named("Items".to_string(), None),
            NbtPathNode::Index(Some(SNBT::Integer(1))),
            NbtPathNode::Named("components".to_string(), None),
            NbtPathNode::Named("minecraft:written_book_content".to_string(), None),
            NbtPathNode::Named("pages".to_string(), None),
            NbtPathNode::Index(Some(SNBT::Integer(3))),
            NbtPathNode::Named("raw".to_string(), None),
        ]);

        assert_eq!(
            path.to_string(),
            r#"Items[1].components.minecraft:written_book_content.pages[3].raw"#
        );
    }

    #[test]
    fn test_root_compound_and_filters() {
        let path = NbtPath(vec![NbtPathNode::RootCompound(compound(vec![(
            "foo",
            snbt_string("4.0f"),
        )]))]);

        assert_eq!(path.to_string(), r#"{"foo":"4.0f"}"#);

        let path2 = NbtPath(vec![
            NbtPathNode::Named(
                "foo".to_string(),
                Some(compound(vec![("bar", snbt_string("baz"))])),
            ),
            NbtPathNode::Named("bar".to_string(), None),
        ]);

        assert_eq!(path2.to_string(), r#"foo{"bar":"baz"}.bar"#);
    }

    #[test]
    fn test_index_all() {
        let path = NbtPath(vec![
            NbtPathNode::Named("foo".to_string(), None),
            NbtPathNode::Named("bar".to_string(), None),
            NbtPathNode::Index(None),
            NbtPathNode::Named("baz".to_string(), None),
        ]);

        assert_eq!(path.to_string(), r#"foo.bar[].baz"#);
    }

    #[test]
    fn test_complex_escaping_with_new_rules() {
        let path_with_quotes = NbtPath(vec![NbtPathNode::Named(
            "key with \"quotes\"".to_string(),
            None,
        )]);
        assert_eq!(path_with_quotes.to_string(), r#""key with \"quotes\"""#);

        let path_with_dot = NbtPath(vec![NbtPathNode::Named("key.with.dot".to_string(), None)]);
        assert_eq!(path_with_dot.to_string(), r#""key.with.dot""#);

        let path_with_slash = NbtPath(vec![NbtPathNode::Named(
            "key with \\ backslash".to_string(),
            None,
        )]);
        assert_eq!(path_with_slash.to_string(), r#""key with \\ backslash""#);
    }
}