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#[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#[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}