cubic-chat 0.1.1

Minecraft components on rust
Documentation
use serde::{Deserialize, Serialize};

use crate::color::{Color, DefaultColor};
use crate::identifier::Identifier;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case", tag = "action", content = "value")]
pub enum ClickEvent {
    OpenUrl(String),
    RunCommand(String),
    SuggestCommand(String),
    ChangePage(usize),
    CopyToClipboard(String),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case", tag = "action", content = "value")]
pub enum HoverEvent {
    ShowText(Box<ComponentType>),
    ShowItem(String),
    ShowEntity(String),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum ComponentType {
    Text(TextComponent),
    Translation(TranslationComponent),
    KeyBind(KeyBindComponent),
    Score(ScoreComponent),
    Selector(SelectorComponent),
    Base(BaseComponent),
}

pub trait Component {
    fn get_base(&self) -> &BaseComponent;
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct BaseComponent {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bold: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub italic: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub underlined: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strikethrough: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub obfuscated: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub font: Option<Identifier>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<Color>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub insertion: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub click_event: Option<ClickEvent>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hover_event: Option<HoverEvent>,
    #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
    pub extra: Vec<ComponentType>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TextComponent {
    pub text: String,
    #[serde(flatten)]
    pub base: BaseComponent,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TranslationComponent {
    pub translate: String,
    #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
    pub with: Vec<ComponentType>,
    #[serde(flatten)]
    pub base: BaseComponent,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct KeyBindComponent {
    pub keybind: String,
    #[serde(flatten)]
    pub base: BaseComponent,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ScoreComponent {
    pub score: Score,
    #[serde(flatten)]
    pub base: BaseComponent,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Score {
    pub name: String,
    pub objective: String,
    pub value: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct SelectorComponent {
    pub selector: String,
    #[serde(flatten)]
    pub base: BaseComponent,
}

macro_rules! component {
    ($name: ident) => {
        impl Component for $name {
            fn get_base(&self) -> &BaseComponent {
                &self.base
            }
        }

        impl $name {
            pub fn reset(&mut self) {
                self.base.bold = Some(false);
                self.base.italic = Some(false);
                self.base.underlined = Some(false);
                self.base.strikethrough = Some(false);
                self.base.obfuscated = Some(false);
                self.base.color = Some(Color::Default(DefaultColor::White));
            }
        }
    };
    ($($name: ident)*) => {
        $(component!($name);)*
    }
}

impl Component for BaseComponent {
    fn get_base(&self) -> &BaseComponent {
        self
    }
}

component!(TextComponent TranslationComponent KeyBindComponent ScoreComponent SelectorComponent);

impl TextComponent {
    pub fn new(text: String) -> Self {
        TextComponent {
            text,
            base: BaseComponent::default(),
        }
    }
}

impl TranslationComponent {
    pub fn new(translate: String) -> Self {
        TranslationComponent {
            translate,
            with: Vec::new(),
            base: BaseComponent::default(),
        }
    }
}

impl KeyBindComponent {
    pub fn new(keybind: String) -> Self {
        KeyBindComponent {
            keybind,
            base: BaseComponent::default(),
        }
    }
}

impl ScoreComponent {
    pub fn new(score: Score) -> Self {
        ScoreComponent {
            score,
            base: BaseComponent::default(),
        }
    }
}

impl SelectorComponent {
    pub fn new(selector: String) -> Self {
        SelectorComponent {
            selector,
            base: BaseComponent::default(),
        }
    }
}

impl From<TextComponent> for ComponentType {
    fn from(component: TextComponent) -> Self {
        ComponentType::Text(component)
    }
}

impl From<TranslationComponent> for ComponentType {
    fn from(component: TranslationComponent) -> Self {
        ComponentType::Translation(component)
    }
}

impl From<KeyBindComponent> for ComponentType {
    fn from(component: KeyBindComponent) -> Self {
        ComponentType::KeyBind(component)
    }
}

impl From<ScoreComponent> for ComponentType {
    fn from(component: ScoreComponent) -> Self {
        ComponentType::Score(component)
    }
}

impl From<SelectorComponent> for ComponentType {
    fn from(component: SelectorComponent) -> Self {
        ComponentType::Selector(component)
    }
}

#[cfg(test)]
mod tests {
    use crate::color::HexColor;

    use super::*;

    #[test]
    pub fn color_ser_test() {
        assert_eq!(serde_json::ser::to_string(&Color::Default(DefaultColor::Black)).unwrap(), "\"black\"");
        assert_eq!(serde_json::ser::to_string(&Color::Default(DefaultColor::Aqua)).unwrap(), "\"aqua\"");
        assert_eq!(serde_json::ser::to_string(&Color::Default(DefaultColor::LightPurple)).unwrap(), "\"light_purple\"");
    }

    #[test]
    pub fn color_de_test() {
        assert_eq!(serde_json::de::from_str::<'_, DefaultColor>("\"black\"").unwrap(), DefaultColor::Black);
        assert_eq!(serde_json::de::from_str::<'_, DefaultColor>("\"light_purple\"").unwrap(), DefaultColor::LightPurple);
    }

    #[test]
    pub fn click_event_ser_test() {
        assert_eq!(
            serde_json::ser::to_string(&ClickEvent::OpenUrl("http://google.com".into())).unwrap(),
            "{\"action\":\"open_url\",\"value\":\"http://google.com\"}"
        );
        assert_eq!(
            serde_json::ser::to_string(&ClickEvent::ChangePage(100)).unwrap(),
            "{\"action\":\"change_page\",\"value\":100}"
        )
    }

    #[test]
    pub fn component_ser_test() {
        let mut component = TextComponent::new("hello".into());
        component.base.bold = Some(true);
        component.base.color = Some(Color::Default(DefaultColor::Aqua));
        assert_eq!(
            serde_json::ser::to_string(&ComponentType::Text(component.clone())).unwrap(),
            "{\"text\":\"hello\",\"bold\":true,\"color\":\"aqua\"}"
        );
        component.base.color = Some(Color::Hex(HexColor::try_from("#ffffff".to_string()).unwrap()));
        assert_eq!(
            serde_json::ser::to_string(&ComponentType::Text(component.clone())).unwrap(),
            "{\"text\":\"hello\",\"bold\":true,\"color\":\"#ffffff\"}"
        );
    }

    #[test]
    pub fn component_de_test() {
        let json_component: ComponentType = serde_json::de::from_str(
            "{\"text\":\"hi\",\"color\":\"red\",\"bold\":true,\"extra\":[{\"text\":\"bye\",\"color\":\"white\",\"bold\":false}]}"
        ).unwrap();
        let mut component = TextComponent::new("hi".into());
        component.base.bold = Some(true);
        component.base.color = Some(DefaultColor::Red.into());
        component.base.extra = vec![
            {
                let mut component = TextComponent::new("bye".into());
                component.base.color = Some(DefaultColor::White.into());
                component.base.bold = Some(false);
                component.into()
            }
        ];
        assert_eq!(json_component, component.into());
    }

}