use std::ops::{BitOr, BitOrAssign};
use serde::{Serialize, Serializer, ser::SerializeMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct Style(u8);
impl Style {
pub const NONE: Self = Self(0);
pub const BOLD: Self = Self(1 << 0);
pub const ITALIC: Self = Self(1 << 1);
pub const UNDERLINE: Self = Self(1 << 2);
pub const STRIKETHROUGH: Self = Self(1 << 3);
pub const SPOILER: Self = Self(1 << 4);
pub const CODE: Self = Self(1 << 5);
const ALL: [(Self, EntityKind); 6] = [
(Self::BOLD, EntityKind::Bold),
(Self::ITALIC, EntityKind::Italic),
(Self::UNDERLINE, EntityKind::Underline),
(Self::STRIKETHROUGH, EntityKind::Strikethrough),
(Self::SPOILER, EntityKind::Spoiler),
(Self::CODE, EntityKind::Code),
];
#[must_use]
pub const fn contains(self, other: Self) -> bool {
other.0 != 0 && (self.0 & other.0) == other.0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub(crate) fn kinds(self) -> Vec<EntityKind> {
Self::ALL
.iter()
.filter(|(flag, _)| self.contains(*flag))
.map(|(_, kind)| kind.clone())
.collect()
}
}
impl BitOr for Style {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Style {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntityKind {
Bold,
Italic,
Underline,
Strikethrough,
Spoiler,
Code,
Pre {
language: Option<String>,
},
TextLink {
url: String,
},
TextMention {
user_id: i64,
},
CustomEmoji {
custom_emoji_id: String,
},
Blockquote,
ExpandableBlockquote,
}
impl EntityKind {
#[must_use]
pub(crate) const fn wire_name(&self) -> &'static str {
match self {
Self::Bold => "bold",
Self::Italic => "italic",
Self::Underline => "underline",
Self::Strikethrough => "strikethrough",
Self::Spoiler => "spoiler",
Self::Code => "code",
Self::Pre { .. } => "pre",
Self::TextLink { .. } => "text_link",
Self::TextMention { .. } => "text_mention",
Self::CustomEmoji { .. } => "custom_emoji",
Self::Blockquote => "blockquote",
Self::ExpandableBlockquote => "expandable_blockquote",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entity {
pub kind: EntityKind,
pub offset: usize,
pub length: usize,
}
impl Entity {
#[must_use]
pub const fn new(kind: EntityKind, offset: usize, length: usize) -> Self {
Self {
kind,
offset,
length,
}
}
#[must_use]
pub const fn end(&self) -> usize {
self.offset + self.length
}
}
impl Serialize for Entity {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let extra_fields: usize = match &self.kind {
EntityKind::Pre { language } => usize::from(language.is_some()),
EntityKind::TextLink { .. }
| EntityKind::TextMention { .. }
| EntityKind::CustomEmoji { .. } => 1,
_ => 0,
};
let mut map: S::SerializeMap = serializer.serialize_map(Some(3 + extra_fields))?;
map.serialize_entry("type", self.kind.wire_name())?;
map.serialize_entry("offset", &self.offset)?;
map.serialize_entry("length", &self.length)?;
match &self.kind {
EntityKind::Pre {
language: Some(language),
} => map.serialize_entry("language", language)?,
EntityKind::TextLink { url } => map.serialize_entry("url", url)?,
EntityKind::TextMention { user_id } => {
map.serialize_entry("user", &TextMentionUser { id: *user_id })?;
}
EntityKind::CustomEmoji { custom_emoji_id } => {
map.serialize_entry("custom_emoji_id", custom_emoji_id)?;
}
_ => {}
}
map.end()
}
}
#[derive(Serialize)]
struct TextMentionUser {
id: i64,
}
#[cfg(test)]
mod tests {
use super::{Entity, EntityKind, Style};
#[test]
fn combined_styles_contain_each_component() {
let style: Style = Style::BOLD | Style::ITALIC;
assert!(style.contains(Style::BOLD));
assert!(style.contains(Style::ITALIC));
assert!(!style.contains(Style::CODE));
}
#[test]
fn empty_style_contains_nothing() {
let style: Style = Style::NONE;
assert!(style.is_empty());
assert!(!style.contains(Style::BOLD));
}
#[test]
fn combined_styles_expand_to_one_kind_each() {
let expected: Vec<EntityKind> = vec![EntityKind::Bold, EntityKind::Italic];
let actual: Vec<EntityKind> = (Style::BOLD | Style::ITALIC).kinds();
assert_eq!(expected, actual);
}
#[test]
fn every_style_flag_expands() {
let all: Style = Style::BOLD
| Style::ITALIC
| Style::UNDERLINE
| Style::STRIKETHROUGH
| Style::SPOILER
| Style::CODE;
let expected: usize = 6;
let actual: usize = all.kinds().len();
assert_eq!(expected, actual);
}
#[test]
fn entity_end_is_offset_plus_length() {
let entity: Entity = Entity::new(EntityKind::Bold, 5, 3);
let expected: usize = 8;
let actual: usize = entity.end();
assert_eq!(expected, actual);
}
#[test]
fn simple_entity_serializes_flat() {
let expected: String = String::from(r#"{"type":"bold","offset":0,"length":4}"#);
let actual: String = serde_json::to_string(&Entity::new(EntityKind::Bold, 0, 4))
.expect("entity should serialize");
assert_eq!(expected, actual);
}
#[test]
fn text_link_serializes_its_url() {
let entity: Entity = Entity::new(
EntityKind::TextLink {
url: String::from("https://example.com"),
},
2,
4,
);
let expected: String = String::from(
r#"{"type":"text_link","offset":2,"length":4,"url":"https://example.com"}"#,
);
let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
assert_eq!(expected, actual);
}
#[test]
fn pre_omits_language_when_absent() {
let entity: Entity = Entity::new(EntityKind::Pre { language: None }, 0, 5);
let expected: String = String::from(r#"{"type":"pre","offset":0,"length":5}"#);
let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
assert_eq!(expected, actual);
}
#[test]
fn pre_includes_language_when_present() {
let entity: Entity = Entity::new(
EntityKind::Pre {
language: Some(String::from("rust")),
},
0,
5,
);
let expected: String =
String::from(r#"{"type":"pre","offset":0,"length":5,"language":"rust"}"#);
let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
assert_eq!(expected, actual);
}
#[test]
fn text_mention_serializes_a_nested_user() {
let entity: Entity = Entity::new(EntityKind::TextMention { user_id: 42 }, 0, 3);
let expected: String =
String::from(r#"{"type":"text_mention","offset":0,"length":3,"user":{"id":42}}"#);
let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
assert_eq!(expected, actual);
}
}