1use serde::{Deserialize, Serialize};
65use serde_json::Error;
66
67#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum Color {
70 Black,
71 DarkBlue,
72 DarkGreen,
73 DarkAqua,
74 DarkRed,
75 DarkPurple,
76 Gold,
77 Gray,
78 DarkGray,
79 Blue,
80 Green,
81 Aqua,
82 Red,
83 LightPurple,
84 Yellow,
85 White,
86}
87
88#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum ClickAction {
91 OpenUrl,
92 RunCommand,
93 SuggestCommand,
94 ChangePage,
95}
96
97#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
98pub struct ClickEvent {
99 pub action: ClickAction,
100 pub value: String,
101}
102
103impl ClickEvent {
104 pub fn new(action: ClickAction, value: &str) -> Self {
105 ClickEvent {
106 action,
107 value: value.to_owned(),
108 }
109 }
110}
111
112#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum HoverAction {
115 ShowText,
116 ShowItem,
117 ShowEntity,
118}
119
120#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
121pub struct HoverEvent {
122 pub action: HoverAction,
123 pub value: String,
124}
125
126impl HoverEvent {
127 pub fn new(action: HoverAction, value: &str) -> Self {
128 HoverEvent {
129 action,
130 value: value.to_owned(),
131 }
132 }
133}
134
135#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
136#[serde(untagged)]
137pub enum Payload {
138 Text {
139 text: String,
140 },
141 Translation {
142 translate: String,
143 with: Vec<Message>,
144 },
145 Keybind {
146 keybind: String,
147 },
148 Score {
149 name: String,
150 objective: String,
151 value: String,
152 },
153 Selector {
154 selector: String,
155 },
156}
157
158impl Payload {
159 pub fn text(text: &str) -> Self {
160 Payload::Text {
161 text: text.to_owned(),
162 }
163 }
164
165 pub fn translation(translate: &str, with: Vec<Message>) -> Self {
166 Payload::Translation {
167 translate: translate.to_owned(),
168 with,
169 }
170 }
171
172 pub fn keybind(keybind: &str) -> Self {
173 Payload::Keybind {
174 keybind: keybind.to_owned(),
175 }
176 }
177
178 pub fn score(name: &str, objective: &str, value: &str) -> Self {
179 Payload::Score {
180 name: name.to_owned(),
181 objective: objective.to_owned(),
182 value: value.to_owned(),
183 }
184 }
185
186 pub fn selector(selector: &str) -> Self {
187 Payload::Selector {
188 selector: selector.to_owned(),
189 }
190 }
191}
192
193#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct Message {
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub bold: Option<bool>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub italic: Option<bool>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub underlined: Option<bool>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub strikethrough: Option<bool>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub obfuscated: Option<bool>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub color: Option<Color>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub insertion: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub click_event: Option<ClickEvent>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub hover_event: Option<HoverEvent>,
214 #[serde(flatten)]
215 pub payload: Payload,
216 #[serde(skip_serializing_if = "Vec::is_empty", default)]
217 pub extra: Vec<Message>,
218}
219
220impl Message {
221 pub fn new(payload: Payload) -> Self {
222 Message {
223 bold: None,
224 italic: None,
225 underlined: None,
226 strikethrough: None,
227 obfuscated: None,
228 color: None,
229 insertion: None,
230 click_event: None,
231 hover_event: None,
232 payload,
233 extra: vec![],
234 }
235 }
236
237 pub fn from_json(json: &str) -> Result<Self, Error> {
238 serde_json::from_str(json)
239 }
240
241 pub fn to_json(&self) -> Result<String, Error> {
242 serde_json::to_string(&self)
243 }
244}
245
246pub struct MessageBuilder {
247 current: Message,
248 root: Option<Message>,
249}
250
251macro_rules! create_builder_style_method (
252 ($style: ident) => (
253 pub fn $style(mut self, value: bool) -> Self {
254 self.current.$style = Some(value);
255 self
256 }
257 );
258);
259
260macro_rules! create_builder_click_event_method (
261 ($method_name: ident, $event: ident) => (
262 pub fn $method_name(mut self, value: &str) -> Self {
263 let click_event = ClickEvent::new(ClickAction::$event, value);
264 self.current.click_event = Some(click_event);
265 self
266 }
267 );
268);
269
270macro_rules! create_builder_hover_event_method (
271 ($method_name: ident, $event: ident) => (
272 pub fn $method_name(mut self, value: &str) -> Self {
273 let hover_event = HoverEvent::new(HoverAction::$event, value);
274 self.current.hover_event = Some(hover_event);
275 self
276 }
277 );
278);
279
280impl MessageBuilder {
281 pub fn builder(payload: Payload) -> Self {
282 let current = Message::new(payload);
283
284 MessageBuilder {
285 current,
286 root: None,
287 }
288 }
289
290 pub fn color(mut self, color: Color) -> Self {
291 self.current.color = Some(color);
292 self
293 }
294
295 pub fn insertion(mut self, insertion: &str) -> Self {
296 self.current.insertion = Some(insertion.to_owned());
297 self
298 }
299
300 create_builder_style_method!(bold);
301 create_builder_style_method!(italic);
302 create_builder_style_method!(underlined);
303 create_builder_style_method!(strikethrough);
304 create_builder_style_method!(obfuscated);
305
306 create_builder_click_event_method!(click_open_url, OpenUrl);
307 create_builder_click_event_method!(click_run_command, RunCommand);
308 create_builder_click_event_method!(click_suggest_command, SuggestCommand);
309 create_builder_click_event_method!(click_change_page, ChangePage);
310
311 create_builder_hover_event_method!(hover_show_text, ShowText);
312 create_builder_hover_event_method!(hover_show_item, ShowItem);
313 create_builder_hover_event_method!(hover_show_entity, ShowEntity);
314
315 pub fn then(mut self, payload: Payload) -> Self {
316 match self.root.as_mut() {
317 Some(root) => {
318 root.extra.push(self.current);
319 }
320 None => {
321 self.root = Some(self.current);
322 }
323 }
324
325 self.current = Message::new(payload);
326 self
327 }
328
329 pub fn build(self) -> Message {
330 match self.root {
331 Some(mut root) => {
332 root.extra.push(self.current);
333 root
334 }
335 None => self.current,
336 }
337 }
338}
339
340#[test]
341fn test_serialize_text_hello_world() {
342 let message = MessageBuilder::builder(Payload::text("Hello"))
343 .color(Color::Yellow)
344 .bold(true)
345 .then(Payload::text("world"))
346 .color(Color::Green)
347 .bold(true)
348 .italic(true)
349 .then(Payload::text("!"))
350 .color(Color::Blue)
351 .build();
352
353 assert_eq!(
354 message.to_json().unwrap(),
355 include_str!("../test/text_hello_world.json")
356 );
357}
358
359#[test]
360fn test_deserialize_text_hello_world() {
361 let expected_message = MessageBuilder::builder(Payload::text("Hello"))
362 .color(Color::Yellow)
363 .bold(true)
364 .then(Payload::text("world"))
365 .color(Color::Green)
366 .bold(true)
367 .italic(true)
368 .then(Payload::text("!"))
369 .color(Color::Blue)
370 .build();
371
372 assert_eq!(
373 expected_message,
374 Message::from_json(include_str!("../test/text_hello_world.json")).unwrap()
375 );
376}
377
378#[test]
379fn test_serialize_translate_opped_steve() {
380 let with = vec![Message::new(Payload::text("Steve"))];
381 let message = Message::new(Payload::translation("Opped %s", with));
382
383 assert_eq!(
384 message.to_json().unwrap(),
385 include_str!("../test/translate_opped_steve.json")
386 );
387}
388
389#[test]
390fn test_deserialize_translate_opped_steve() {
391 let with = vec![Message::new(Payload::text("Steve"))];
392 let expected_message = Message::new(Payload::translation("Opped %s", with));
393
394 assert_eq!(
395 expected_message,
396 Message::from_json(include_str!("../test/translate_opped_steve.json")).unwrap()
397 );
398}
399
400#[test]
401fn test_serialize_keybind_jump() {
402 let message = MessageBuilder::builder(Payload::text("Press \""))
403 .color(Color::Yellow)
404 .bold(true)
405 .then(Payload::keybind("key.jump"))
406 .color(Color::Blue)
407 .bold(false)
408 .underlined(true)
409 .then(Payload::text("\" to jump!"))
410 .build();
411
412 assert_eq!(
413 message.to_json().unwrap(),
414 include_str!("../test/keybind_jump.json")
415 );
416}
417
418#[test]
419fn test_deserialize_keybind_jump() {
420 let expected_message = MessageBuilder::builder(Payload::text("Press \""))
421 .color(Color::Yellow)
422 .bold(true)
423 .then(Payload::keybind("key.jump"))
424 .color(Color::Blue)
425 .bold(false)
426 .underlined(true)
427 .then(Payload::text("\" to jump!"))
428 .build();
429
430 assert_eq!(
431 expected_message,
432 Message::from_json(include_str!("../test/keybind_jump.json")).unwrap()
433 );
434}
435
436#[test]
437fn test_serialize_click_open_url() {
438 let message = MessageBuilder::builder(Payload::text("click me"))
439 .color(Color::Yellow)
440 .bold(true)
441 .click_open_url("http://minecraft.net")
442 .build();
443
444 assert_eq!(
445 message.to_json().unwrap(),
446 include_str!("../test/click_open_url.json")
447 );
448}
449
450#[test]
451fn test_deserialize_click_open_url() {
452 let expected_message = MessageBuilder::builder(Payload::text("click me"))
453 .color(Color::Yellow)
454 .bold(true)
455 .click_open_url("http://minecraft.net")
456 .build();
457
458 assert_eq!(
459 expected_message,
460 Message::from_json(include_str!("../test/click_open_url.json")).unwrap()
461 );
462}
463
464#[test]
465fn test_serialize_click_run_command() {
466 let message = MessageBuilder::builder(Payload::text("click me"))
467 .color(Color::LightPurple)
468 .italic(true)
469 .click_run_command("/help")
470 .build();
471
472 assert_eq!(
473 message.to_json().unwrap(),
474 include_str!("../test/click_run_command.json")
475 );
476}
477
478#[test]
479fn test_deserialize_click_run_command() {
480 let expected_message = MessageBuilder::builder(Payload::text("click me"))
481 .color(Color::LightPurple)
482 .italic(true)
483 .click_run_command("/help")
484 .build();
485
486 assert_eq!(
487 expected_message,
488 Message::from_json(include_str!("../test/click_run_command.json")).unwrap()
489 );
490}
491
492#[test]
493fn test_serialize_click_suggest_command() {
494 let message = MessageBuilder::builder(Payload::text("click me"))
495 .color(Color::Blue)
496 .obfuscated(true)
497 .click_suggest_command("/help")
498 .build();
499
500 assert_eq!(
501 message.to_json().unwrap(),
502 include_str!("../test/click_suggest_command.json")
503 );
504}
505
506#[test]
507fn test_deserialize_click_suggest_command() {
508 let expected_message = MessageBuilder::builder(Payload::text("click me"))
509 .color(Color::Blue)
510 .obfuscated(true)
511 .click_suggest_command("/help")
512 .build();
513
514 assert_eq!(
515 expected_message,
516 Message::from_json(include_str!("../test/click_suggest_command.json")).unwrap()
517 );
518}
519
520#[test]
521fn test_serialize_click_change_page() {
522 let message = MessageBuilder::builder(Payload::text("click me"))
523 .color(Color::DarkGray)
524 .underlined(true)
525 .click_change_page("2")
526 .build();
527
528 assert_eq!(
529 message.to_json().unwrap(),
530 include_str!("../test/click_change_page.json")
531 );
532}
533
534#[test]
535fn test_deserialize_click_change_page() {
536 let expected_message = MessageBuilder::builder(Payload::text("click me"))
537 .color(Color::DarkGray)
538 .underlined(true)
539 .click_change_page("2")
540 .build();
541
542 assert_eq!(
543 expected_message,
544 Message::from_json(include_str!("../test/click_change_page.json")).unwrap()
545 );
546}
547
548#[test]
549fn test_serialize_hover_show_text() {
550 let message = MessageBuilder::builder(Payload::text("hover at me"))
551 .color(Color::DarkPurple)
552 .bold(true)
553 .hover_show_text("Herobrine behind you!")
554 .build();
555
556 assert_eq!(
557 message.to_json().unwrap(),
558 include_str!("../test/hover_show_text.json")
559 );
560}
561
562#[test]
563fn test_deserialize_hover_show_text() {
564 let expected_message = MessageBuilder::builder(Payload::text("hover at me"))
565 .color(Color::DarkPurple)
566 .bold(true)
567 .hover_show_text("Herobrine behind you!")
568 .build();
569
570 assert_eq!(
571 expected_message,
572 Message::from_json(include_str!("../test/hover_show_text.json")).unwrap()
573 );
574}
575
576#[test]
577fn test_serialize_hover_show_item() {
578 let message = MessageBuilder::builder(Payload::text("hover at me"))
579 .color(Color::DarkRed)
580 .italic(true)
581 .hover_show_item("{\"id\":\"stone\",\"Count\":1}")
582 .build();
583
584 assert_eq!(
585 message.to_json().unwrap(),
586 include_str!("../test/hover_show_item.json")
587 );
588}
589
590#[test]
591fn test_deserialize_hover_show_item() {
592 let expected_message = MessageBuilder::builder(Payload::text("hover at me"))
593 .color(Color::DarkRed)
594 .italic(true)
595 .hover_show_item("{\"id\":\"stone\",\"Count\":1}")
596 .build();
597
598 assert_eq!(
599 expected_message,
600 Message::from_json(include_str!("../test/hover_show_item.json")).unwrap()
601 );
602}
603
604#[test]
605fn test_serialize_hover_show_entity() {
606 let message = MessageBuilder::builder(Payload::text("hover at me"))
607 .color(Color::DarkAqua)
608 .obfuscated(true)
609 .hover_show_entity("{\"id\":\"7e4a61cc-83fa-4441-a299-bf69786e610a\",\"type\":\"minecraft:zombie\",\"name\":\"Zombie}\"")
610 .build();
611
612 assert_eq!(
613 message.to_json().unwrap(),
614 include_str!("../test/hover_show_entity.json")
615 );
616}
617
618#[test]
619fn test_deserialize_hover_show_entity() {
620 let expected_message = MessageBuilder::builder(Payload::text("hover at me"))
621 .color(Color::DarkAqua)
622 .obfuscated(true)
623 .hover_show_entity("{\"id\":\"7e4a61cc-83fa-4441-a299-bf69786e610a\",\"type\":\"minecraft:zombie\",\"name\":\"Zombie}\"")
624 .build();
625
626 assert_eq!(
627 expected_message,
628 Message::from_json(include_str!("../test/hover_show_entity.json")).unwrap()
629 );
630}