minecraft_command_types/
nbt_path.rs

1use crate::snbt::{SNBT, fmt_snbt_compound};
2use minecraft_command_types_derive::HasMacro;
3use std::collections::BTreeMap;
4use std::fmt::{Display, Formatter};
5
6pub type SNBTCompound = BTreeMap<String, SNBT>;
7
8fn escape_nbt_path_key(name: &str) -> String {
9    let needs_quotes = name
10        .chars()
11        .any(|c| matches!(c, ' ' | '"' | '\'' | '[' | ']' | '.' | '{' | '}'));
12
13    if needs_quotes {
14        let escaped_content = name.replace('\\', "\\\\").replace('"', "\\\"");
15        format!("\"{}\"", escaped_content)
16    } else {
17        name.to_string()
18    }
19}
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
22pub enum NbtPathNode {
23    RootCompound(SNBTCompound),
24    Named(String, Option<SNBTCompound>),
25    Index(Option<SNBT>),
26}
27
28#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
29pub struct NbtPath(pub Vec<NbtPathNode>);
30
31impl Display for NbtPathNode {
32    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33        match self {
34            NbtPathNode::RootCompound(compound) => fmt_snbt_compound(f, compound),
35            NbtPathNode::Named(name, filter) => {
36                write!(f, "{}", escape_nbt_path_key(name))?;
37
38                if let Some(filter) = filter
39                    && !filter.is_empty()
40                {
41                    fmt_snbt_compound(f, filter)?;
42                }
43                Ok(())
44            }
45            NbtPathNode::Index(Some(snbt)) => write!(f, "[{}]", snbt),
46            NbtPathNode::Index(None) => write!(f, "[]"),
47        }
48    }
49}
50
51impl Display for NbtPath {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        let mut first = true;
54        for node in &self.0 {
55            if !first && !matches!(node, NbtPathNode::Index(_)) {
56                write!(f, ".")?;
57            }
58            first = false;
59            write!(f, "{}", node)?;
60        }
61        Ok(())
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::snbt::SNBT;
69    use std::collections::BTreeMap;
70
71    fn snbt_string(s: &str) -> SNBT {
72        SNBT::String(s.to_string())
73    }
74
75    fn compound(pairs: Vec<(&str, SNBT)>) -> SNBTCompound {
76        let mut map = BTreeMap::new();
77        for (k, v) in pairs {
78            map.insert(k.to_string(), v);
79        }
80        map
81    }
82
83    #[test]
84    fn test_example_1() {
85        let path = NbtPath(vec![
86            NbtPathNode::Named("foo".to_string(), None),
87            NbtPathNode::Named("bar".to_string(), None),
88            NbtPathNode::Index(Some(SNBT::Integer(0))),
89            NbtPathNode::Named("A [crazy name]!".to_string(), None),
90            NbtPathNode::Named("baz".to_string(), None),
91        ]);
92
93        assert_eq!(path.to_string(), r#"foo.bar[0]."A [crazy name]!".baz"#);
94    }
95
96    #[test]
97    fn test_example_2() {
98        let path = NbtPath(vec![
99            NbtPathNode::Named("Items".to_string(), None),
100            NbtPathNode::Index(Some(SNBT::Integer(1))),
101            NbtPathNode::Named("components".to_string(), None),
102            NbtPathNode::Named("minecraft:written_book_content".to_string(), None),
103            NbtPathNode::Named("pages".to_string(), None),
104            NbtPathNode::Index(Some(SNBT::Integer(3))),
105            NbtPathNode::Named("raw".to_string(), None),
106        ]);
107
108        assert_eq!(
109            path.to_string(),
110            r#"Items[1].components.minecraft:written_book_content.pages[3].raw"#
111        );
112    }
113
114    #[test]
115    fn test_root_compound_and_filters() {
116        let path = NbtPath(vec![NbtPathNode::RootCompound(compound(vec![(
117            "foo",
118            snbt_string("4.0f"),
119        )]))]);
120
121        assert_eq!(path.to_string(), r#"{"foo":"4.0f"}"#);
122
123        let path2 = NbtPath(vec![
124            NbtPathNode::Named(
125                "foo".to_string(),
126                Some(compound(vec![("bar", snbt_string("baz"))])),
127            ),
128            NbtPathNode::Named("bar".to_string(), None),
129        ]);
130
131        assert_eq!(path2.to_string(), r#"foo{"bar":"baz"}.bar"#);
132    }
133
134    #[test]
135    fn test_index_all() {
136        let path = NbtPath(vec![
137            NbtPathNode::Named("foo".to_string(), None),
138            NbtPathNode::Named("bar".to_string(), None),
139            NbtPathNode::Index(None),
140            NbtPathNode::Named("baz".to_string(), None),
141        ]);
142
143        assert_eq!(path.to_string(), r#"foo.bar[].baz"#);
144    }
145
146    #[test]
147    fn test_complex_escaping_with_new_rules() {
148        let path_with_quotes = NbtPath(vec![NbtPathNode::Named(
149            "key with \"quotes\"".to_string(),
150            None,
151        )]);
152        assert_eq!(path_with_quotes.to_string(), r#""key with \"quotes\"""#);
153
154        let path_with_dot = NbtPath(vec![NbtPathNode::Named("key.with.dot".to_string(), None)]);
155        assert_eq!(path_with_dot.to_string(), r#""key.with.dot""#);
156
157        let path_with_slash = NbtPath(vec![NbtPathNode::Named(
158            "key with \\ backslash".to_string(),
159            None,
160        )]);
161        assert_eq!(path_with_slash.to_string(), r#""key with \\ backslash""#);
162    }
163}