Skip to main content

minecraft_pinger/java/
model.rs

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// Components
17#[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// Components end
63
64#[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}