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