minecraft_chat/
lib.rs

1//! Minecraft chat are represented as json object. It's used in different packets.
2//! Information about format can be found at https://wiki.vg/Chat.
3//!
4//! # Example
5//!
6//! ## Serialize
7//!
8//! ```
9//! use minecraft_chat::{MessageBuilder, Payload, Color};
10//!
11//! let message = MessageBuilder::builder(Payload::text("Hello"))
12//!    .color(Color::Yellow)
13//!    .bold(true)
14//!    .then(Payload::text("world"))
15//!    .color(Color::Green)
16//!    .bold(true)
17//!    .italic(true)
18//!    .then(Payload::text("!"))
19//!    .color(Color::Blue)
20//!    .build();
21//!
22//! println!("{}", message.to_json().unwrap());
23//! ```
24//!
25//! ## Deserialize
26//!
27//! ```
28//! use minecraft_chat::{MessageBuilder, Color, Payload, Message};
29//!
30//! let json = r#"
31//! {
32//!   "bold":true,
33//!   "color":"yellow",
34//!   "text":"Hello",
35//!   "extra":[
36//!      {
37//!         "bold":true,
38//!         "italic":true,
39//!         "color":"green",
40//!         "text":"world"
41//!      },
42//!      {
43//!         "color":"blue",
44//!         "text":"!"
45//!      }
46//!   ]
47//! }
48//! "#;
49//!
50//! let expected_message = MessageBuilder::builder(Payload::text("Hello"))
51//!    .color(Color::Yellow)
52//!    .bold(true)
53//!    .then(Payload::text("world"))
54//!    .color(Color::Green)
55//!    .bold(true)
56//!    .italic(true)
57//!    .then(Payload::text("!"))
58//!    .color(Color::Blue)
59//!    .build();
60//!
61//! assert_eq!(expected_message, Message::from_json(json).unwrap());
62//! ```
63
64use 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}