Skip to main content

minecraft_pinger/java/
model.rs

1use serde::{Deserialize, Serialize};
2use std::net::SocketAddr;
3
4#[derive(Serialize, Deserialize)]
5pub struct JavaPing {
6    pub version: Version,
7    pub players: Players,
8    pub description: Description,
9    pub favicon: Option<String>,
10    #[serde(rename = "modinfo")]
11    pub mod_info: Option<ModInfo>,
12
13    #[serde(default)]
14    pub latency: u32,
15
16    #[serde(skip_serializing, skip_deserializing)]
17    pub connected_addr: Option<SocketAddr>,
18}
19
20// Components
21#[derive(Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum Description {
24    Plain(String),
25    Component(TextComponent),
26}
27
28impl Description {
29    pub fn to_plain_text(&self) -> String {
30        match self {
31            Description::Plain(s) => s.clone(),
32            Description::Component(c) => c.to_plain_text(),
33        }
34    }
35}
36
37#[derive(Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum TextComponent {
40    Object {
41        #[serde(default)]
42        text: String,
43
44        #[serde(skip_serializing_if = "Option::is_none")]
45        hat: Option<bool>,
46        #[serde(skip_serializing_if = "Option::is_none")]
47        player: Option<PlayerField>,
48        #[serde(skip_serializing_if = "Option::is_none", alias = "shadowColor")]
49        shadow_color: Option<i32>,
50        #[serde(skip_serializing_if = "Option::is_none")]
51        atlas: Option<String>,
52        #[serde(skip_serializing_if = "Option::is_none")]
53        sprite: Option<String>,
54        #[serde(skip_serializing_if = "Option::is_none")]
55        bold: Option<bool>,
56        #[serde(skip_serializing_if = "Option::is_none")]
57        strikethrough: Option<bool>,
58        #[serde(skip_serializing_if = "Option::is_none")]
59        obfuscated: Option<bool>,
60        #[serde(skip_serializing_if = "Option::is_none")]
61        italic: Option<bool>,
62        #[serde(skip_serializing_if = "Option::is_none")]
63        underlined: Option<bool>,
64        #[serde(skip_serializing_if = "Option::is_none")]
65        font: Option<String>,
66
67        #[serde(skip_serializing_if = "Option::is_none")]
68        color: Option<String>,
69        #[serde(skip_serializing_if = "Option::is_none")]
70        extra: Option<Vec<TextComponent>>,
71    },
72    String(String),
73    Array(Vec<TextComponent>),
74}
75
76impl TextComponent {
77    pub fn to_plain_text(&self) -> String {
78        let mut out = String::new();
79        self.write_plain_text(&mut out);
80        out
81    }
82
83    fn write_plain_text(&self, out: &mut String) {
84        match self {
85            TextComponent::String(s) => out.push_str(s),
86            TextComponent::Array(items) => {
87                for item in items {
88                    item.write_plain_text(out);
89                }
90            }
91            TextComponent::Object { text, extra, .. } => {
92                out.push_str(text);
93                if let Some(extra) = extra {
94                    for item in extra {
95                        item.write_plain_text(out);
96                    }
97                }
98            }
99        }
100    }
101}
102// Components end
103
104#[derive(Serialize, Deserialize)]
105#[serde(untagged)]
106pub enum PlayerField {
107    Name(String),
108    Full(Player),
109}
110
111#[derive(Serialize, Deserialize)]
112pub struct Player {
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub name: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub properties: Option<Vec<Property>>,
117}
118
119#[derive(Serialize, Deserialize)]
120pub struct Property {
121    pub name: String,
122    pub value: String,
123}
124
125#[derive(Serialize, Deserialize)]
126pub struct Version {
127    pub name: String,
128    pub protocol: Option<i32>,
129}
130
131#[derive(Serialize, Deserialize)]
132pub struct Players {
133    pub online: u32,
134    pub max: i32,
135    pub sample: Option<Vec<PlayerInfo>>,
136}
137
138#[derive(Serialize, Deserialize)]
139pub struct PlayerInfo {
140    pub name: String,
141    pub id: String,
142}
143
144#[derive(Serialize, Deserialize, Default)]
145pub struct ModInfo {
146    #[serde(rename = "type", default)]
147    pub name: String,
148    #[serde(rename = "modList", default)]
149    pub mod_list: Vec<Mod>,
150}
151
152#[derive(Serialize, Deserialize)]
153pub struct Mod {
154    #[serde(rename = "modid")]
155    pub mod_id: String,
156    pub version: String,
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use serde_json::json;
163
164    #[test]
165    fn test_java_ping_deserialize_plain_description() {
166        let json = json!({
167            "version": {"name": "1.20.1", "protocol": 775},
168            "players": {"online": 5, "max": 20, "sample": null},
169            "description": "A Minecraft Server",
170            "favicon": null,
171            "modinfo": null
172        });
173
174        let ping: JavaPing = serde_json::from_value(json).unwrap();
175        assert_eq!(ping.version.name, "1.20.1");
176        assert_eq!(ping.version.protocol, Some(775));
177        assert_eq!(ping.players.online, 5);
178        assert_eq!(ping.players.max, 20);
179
180        match ping.description {
181            Description::Plain(s) => assert_eq!(s, "A Minecraft Server"),
182            Description::Component(_) => panic!("Expected plain description"),
183        }
184    }
185
186    #[test]
187    fn test_java_ping_deserialize_component_description() {
188        let json = json!({
189            "version": {"name": "1.20.1", "protocol": 775},
190            "players": {"online": 5, "max": 20, "sample": null},
191            "description": {"text": "A Minecraft Server", "color": "green"},
192            "favicon": null,
193            "modinfo": null
194        });
195
196        let ping: JavaPing = serde_json::from_value(json).unwrap();
197
198        match ping.description {
199            Description::Component(TextComponent::Object { text, color, .. }) => {
200                assert_eq!(text, "A Minecraft Server");
201                assert_eq!(color, Some("green".to_string()));
202            }
203            _ => panic!("Expected component description"),
204        }
205    }
206
207    #[test]
208    fn test_java_ping_with_favicon() {
209        let json = json!({
210            "version": {"name": "1.20.1", "protocol": 775},
211            "players": {"online": 5, "max": 20, "sample": null},
212            "description": "Test",
213            "favicon": "data:image/png;base64,abc123",
214            "modinfo": null
215        });
216
217        let ping: JavaPing = serde_json::from_value(json).unwrap();
218        assert_eq!(
219            ping.favicon,
220            Some("data:image/png;base64,abc123".to_string())
221        );
222    }
223
224    #[test]
225    fn test_java_ping_with_modinfo() {
226        let json = json!({
227            "version": {"name": "1.20.1", "protocol": 775},
228            "players": {"online": 5, "max": 20, "sample": null},
229            "description": "Test",
230            "favicon": null,
231            "modinfo": {"type": "FML", "modList": [{"modid": "testmod", "version": "1.0"}]}
232        });
233
234        let ping: JavaPing = serde_json::from_value(json).unwrap();
235        assert!(ping.mod_info.is_some());
236        let mod_info = ping.mod_info.unwrap();
237        assert_eq!(mod_info.name, "FML");
238        assert_eq!(mod_info.mod_list.len(), 1);
239        assert_eq!(mod_info.mod_list[0].mod_id, "testmod");
240        assert_eq!(mod_info.mod_list[0].version, "1.0");
241    }
242
243    #[test]
244    fn test_java_ping_with_player_sample() {
245        let json = json!({
246            "version": {"name": "1.20.1", "protocol": 775},
247            "players": {
248                "online": 5,
249                "max": 20,
250                "sample": [
251                    {"name": "Player1", "id": "uuid1"},
252                    {"name": "Player2", "id": "uuid2"}
253                ]
254            },
255            "description": "Test",
256            "favicon": null,
257            "modinfo": null
258        });
259
260        let ping: JavaPing = serde_json::from_value(json).unwrap();
261        assert_eq!(ping.players.sample.as_ref().unwrap().len(), 2);
262        assert_eq!(ping.players.sample.as_ref().unwrap()[0].name, "Player1");
263    }
264
265    #[test]
266    fn test_text_component_string_variant() {
267        let json = json!("simple string");
268        let component: TextComponent = serde_json::from_value(json).unwrap();
269
270        match component {
271            TextComponent::String(s) => assert_eq!(s, "simple string"),
272            _ => panic!("Expected string variant"),
273        }
274    }
275
276    #[test]
277    fn test_text_component_array_variant() {
278        let json = json!([{"text": "part1"}, {"text": "part2"}]);
279        let component: TextComponent = serde_json::from_value(json).unwrap();
280
281        match component {
282            TextComponent::Array(arr) => {
283                assert_eq!(arr.len(), 2);
284                match &arr[0] {
285                    TextComponent::Object { text, .. } => assert_eq!(text, "part1"),
286                    _ => panic!("Expected object"),
287                }
288            }
289            _ => panic!("Expected array variant"),
290        }
291    }
292
293    #[test]
294    fn test_version_deserialize() {
295        let json = json!({"name": "1.20.1", "protocol": 775});
296        let version: Version = serde_json::from_value(json).unwrap();
297        assert_eq!(version.name, "1.20.1");
298        assert_eq!(version.protocol, Some(775));
299    }
300
301    #[test]
302    fn test_version_deserialize_without_protocol() {
303        let json = json!({"name": "1.20.1"});
304        let version: Version = serde_json::from_value(json).unwrap();
305        assert_eq!(version.name, "1.20.1");
306        assert_eq!(version.protocol, None);
307    }
308
309    #[test]
310    fn test_players_deserialize() {
311        let json = json!({"online": 10, "max": 20, "sample": []});
312        let players: Players = serde_json::from_value(json).unwrap();
313        assert_eq!(players.online, 10);
314        assert_eq!(players.max, 20);
315        assert!(players.sample.is_some());
316    }
317
318    #[test]
319    fn test_description_to_plain_text_plain() {
320        let description = Description::Plain("A Minecraft Server".to_string());
321        assert_eq!(description.to_plain_text(), "A Minecraft Server");
322    }
323
324    #[test]
325    fn test_description_to_plain_text_component_with_extra() {
326        let json = json!({
327            "text": "Hello ",
328            "color": "green",
329            "extra": [{"text": "World"}, ["!", {"text": "!"}]]
330        });
331        let component: TextComponent = serde_json::from_value(json).unwrap();
332        let description = Description::Component(component);
333        assert_eq!(description.to_plain_text(), "Hello World!!");
334    }
335
336    #[test]
337    fn test_text_component_to_plain_text_string_variant() {
338        let component = TextComponent::String("simple".to_string());
339        assert_eq!(component.to_plain_text(), "simple");
340    }
341}