Skip to main content

minecraft_pinger/java/
model.rs

1use std::net::SocketAddr;
2use serde::{Deserialize, Serialize};
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
28#[derive(Serialize, Deserialize)]
29#[serde(untagged)]
30pub enum TextComponent {
31    Object {
32        #[serde(default)]
33        text: String,
34
35        #[serde(skip_serializing_if = "Option::is_none")]
36        hat: Option<bool>,
37        #[serde(skip_serializing_if = "Option::is_none")]
38        player: Option<PlayerField>,
39        #[serde(skip_serializing_if = "Option::is_none", alias = "shadowColor")]
40        shadow_color: Option<i32>,
41        #[serde(skip_serializing_if = "Option::is_none")]
42        atlas: Option<String>,
43        #[serde(skip_serializing_if = "Option::is_none")]
44        sprite: Option<String>,
45        #[serde(skip_serializing_if = "Option::is_none")]
46        bold: Option<bool>,
47        #[serde(skip_serializing_if = "Option::is_none")]
48        strikethrough: Option<bool>,
49        #[serde(skip_serializing_if = "Option::is_none")]
50        obfuscated: Option<bool>,
51        #[serde(skip_serializing_if = "Option::is_none")]
52        italic: Option<bool>,
53        #[serde(skip_serializing_if = "Option::is_none")]
54        underlined: Option<bool>,
55        #[serde(skip_serializing_if = "Option::is_none")]
56        font: Option<String>,
57
58        #[serde(skip_serializing_if = "Option::is_none")]
59        color: Option<String>,
60        #[serde(skip_serializing_if = "Option::is_none")]
61        extra: Option<Vec<TextComponent>>,
62    },
63    String(String),
64    Array(Vec<TextComponent>),
65}
66// Components end
67
68#[derive(Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum PlayerField {
71    Name(String),
72    Full(Player),
73}
74
75#[derive(Serialize, Deserialize)]
76pub struct Player {
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub name: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub properties: Option<Vec<Property>>,
81}
82
83#[derive(Serialize, Deserialize)]
84pub struct Property {
85    pub name: String,
86    pub value: String,
87}
88
89#[derive(Serialize, Deserialize)]
90pub struct Version {
91    pub name: String,
92    pub protocol: Option<i32>,
93}
94
95#[derive(Serialize, Deserialize)]
96pub struct Players {
97    pub online: u32,
98    pub max: i32,
99    pub sample: Option<Vec<PlayerInfo>>,
100}
101
102#[derive(Serialize, Deserialize)]
103pub struct PlayerInfo {
104    pub name: String,
105    pub id: String,
106}
107
108#[derive(Serialize, Deserialize, Default)]
109pub struct ModInfo {
110    #[serde(rename = "type", default)]
111    pub name: String,
112    #[serde(rename = "modList", default)]
113    pub mod_list: Vec<Mod>,
114}
115
116#[derive(Serialize, Deserialize)]
117pub struct Mod {
118    #[serde(rename = "modid")]
119    pub mod_id: String,
120    pub version: String,
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use serde_json::json;
127
128    #[test]
129    fn test_java_ping_deserialize_plain_description() {
130        let json = json!({
131            "version": {"name": "1.20.1", "protocol": 775},
132            "players": {"online": 5, "max": 20, "sample": null},
133            "description": "A Minecraft Server",
134            "favicon": null,
135            "modinfo": null
136        });
137
138        let ping: JavaPing = serde_json::from_value(json).unwrap();
139        assert_eq!(ping.version.name, "1.20.1");
140        assert_eq!(ping.version.protocol, Some(775));
141        assert_eq!(ping.players.online, 5);
142        assert_eq!(ping.players.max, 20);
143
144        match ping.description {
145            Description::Plain(s) => assert_eq!(s, "A Minecraft Server"),
146            Description::Component(_) => panic!("Expected plain description"),
147        }
148    }
149
150    #[test]
151    fn test_java_ping_deserialize_component_description() {
152        let json = json!({
153            "version": {"name": "1.20.1", "protocol": 775},
154            "players": {"online": 5, "max": 20, "sample": null},
155            "description": {"text": "A Minecraft Server", "color": "green"},
156            "favicon": null,
157            "modinfo": null
158        });
159
160        let ping: JavaPing = serde_json::from_value(json).unwrap();
161
162        match ping.description {
163            Description::Component(TextComponent::Object { text, color, .. }) => {
164                assert_eq!(text, "A Minecraft Server");
165                assert_eq!(color, Some("green".to_string()));
166            }
167            _ => panic!("Expected component description"),
168        }
169    }
170
171    #[test]
172    fn test_java_ping_with_favicon() {
173        let json = json!({
174            "version": {"name": "1.20.1", "protocol": 775},
175            "players": {"online": 5, "max": 20, "sample": null},
176            "description": "Test",
177            "favicon": "data:image/png;base64,abc123",
178            "modinfo": null
179        });
180
181        let ping: JavaPing = serde_json::from_value(json).unwrap();
182        assert_eq!(
183            ping.favicon,
184            Some("data:image/png;base64,abc123".to_string())
185        );
186    }
187
188    #[test]
189    fn test_java_ping_with_modinfo() {
190        let json = json!({
191            "version": {"name": "1.20.1", "protocol": 775},
192            "players": {"online": 5, "max": 20, "sample": null},
193            "description": "Test",
194            "favicon": null,
195            "modinfo": {"type": "FML", "modList": [{"modid": "testmod", "version": "1.0"}]}
196        });
197
198        let ping: JavaPing = serde_json::from_value(json).unwrap();
199        assert!(ping.mod_info.is_some());
200        let mod_info = ping.mod_info.unwrap();
201        assert_eq!(mod_info.name, "FML");
202        assert_eq!(mod_info.mod_list.len(), 1);
203        assert_eq!(mod_info.mod_list[0].mod_id, "testmod");
204        assert_eq!(mod_info.mod_list[0].version, "1.0");
205    }
206
207    #[test]
208    fn test_java_ping_with_player_sample() {
209        let json = json!({
210            "version": {"name": "1.20.1", "protocol": 775},
211            "players": {
212                "online": 5,
213                "max": 20,
214                "sample": [
215                    {"name": "Player1", "id": "uuid1"},
216                    {"name": "Player2", "id": "uuid2"}
217                ]
218            },
219            "description": "Test",
220            "favicon": null,
221            "modinfo": null
222        });
223
224        let ping: JavaPing = serde_json::from_value(json).unwrap();
225        assert_eq!(ping.players.sample.as_ref().unwrap().len(), 2);
226        assert_eq!(ping.players.sample.as_ref().unwrap()[0].name, "Player1");
227    }
228
229    #[test]
230    fn test_text_component_string_variant() {
231        let json = json!("simple string");
232        let component: TextComponent = serde_json::from_value(json).unwrap();
233
234        match component {
235            TextComponent::String(s) => assert_eq!(s, "simple string"),
236            _ => panic!("Expected string variant"),
237        }
238    }
239
240    #[test]
241    fn test_text_component_array_variant() {
242        let json = json!([{"text": "part1"}, {"text": "part2"}]);
243        let component: TextComponent = serde_json::from_value(json).unwrap();
244
245        match component {
246            TextComponent::Array(arr) => {
247                assert_eq!(arr.len(), 2);
248                match &arr[0] {
249                    TextComponent::Object { text, .. } => assert_eq!(text, "part1"),
250                    _ => panic!("Expected object"),
251                }
252            }
253            _ => panic!("Expected array variant"),
254        }
255    }
256
257    #[test]
258    fn test_version_deserialize() {
259        let json = json!({"name": "1.20.1", "protocol": 775});
260        let version: Version = serde_json::from_value(json).unwrap();
261        assert_eq!(version.name, "1.20.1");
262        assert_eq!(version.protocol, Some(775));
263    }
264
265    #[test]
266    fn test_version_deserialize_without_protocol() {
267        let json = json!({"name": "1.20.1"});
268        let version: Version = serde_json::from_value(json).unwrap();
269        assert_eq!(version.name, "1.20.1");
270        assert_eq!(version.protocol, None);
271    }
272
273    #[test]
274    fn test_players_deserialize() {
275        let json = json!({"online": 10, "max": 20, "sample": []});
276        let players: Players = serde_json::from_value(json).unwrap();
277        assert_eq!(players.online, 10);
278        assert_eq!(players.max, 20);
279        assert!(players.sample.is_some());
280    }
281}