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#[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#[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: i32,
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}