1use std::ops::{BitOr, BitOrAssign};
2
3use serde::{Serialize, Serializer, ser::SerializeMap};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
14pub struct Style(u8);
15
16impl Style {
17 pub const NONE: Self = Self(0);
19 pub const BOLD: Self = Self(1 << 0);
21 pub const ITALIC: Self = Self(1 << 1);
23 pub const UNDERLINE: Self = Self(1 << 2);
25 pub const STRIKETHROUGH: Self = Self(1 << 3);
27 pub const SPOILER: Self = Self(1 << 4);
29 pub const CODE: Self = Self(1 << 5);
31
32 const ALL: [(Self, EntityKind); 6] = [
34 (Self::BOLD, EntityKind::Bold),
35 (Self::ITALIC, EntityKind::Italic),
36 (Self::UNDERLINE, EntityKind::Underline),
37 (Self::STRIKETHROUGH, EntityKind::Strikethrough),
38 (Self::SPOILER, EntityKind::Spoiler),
39 (Self::CODE, EntityKind::Code),
40 ];
41
42 #[must_use]
44 pub const fn contains(self, other: Self) -> bool {
45 other.0 != 0 && (self.0 & other.0) == other.0
46 }
47
48 #[must_use]
50 pub const fn is_empty(self) -> bool {
51 self.0 == 0
52 }
53
54 #[must_use]
56 pub(crate) fn kinds(self) -> Vec<EntityKind> {
57 Self::ALL
58 .iter()
59 .filter(|(flag, _)| self.contains(*flag))
60 .map(|(_, kind)| kind.clone())
61 .collect()
62 }
63}
64
65impl BitOr for Style {
66 type Output = Self;
67
68 fn bitor(self, rhs: Self) -> Self::Output {
69 Self(self.0 | rhs.0)
70 }
71}
72
73impl BitOrAssign for Style {
74 fn bitor_assign(&mut self, rhs: Self) {
75 self.0 |= rhs.0;
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum EntityKind {
82 Bold,
84 Italic,
86 Underline,
88 Strikethrough,
90 Spoiler,
92 Code,
94 Pre {
99 language: Option<String>,
101 },
102 TextLink {
104 url: String,
106 },
107 TextMention {
109 user_id: i64,
111 },
112 CustomEmoji {
114 custom_emoji_id: String,
116 },
117 Blockquote,
119 ExpandableBlockquote,
121}
122
123impl EntityKind {
124 #[must_use]
126 pub(crate) const fn wire_name(&self) -> &'static str {
127 match self {
128 Self::Bold => "bold",
129 Self::Italic => "italic",
130 Self::Underline => "underline",
131 Self::Strikethrough => "strikethrough",
132 Self::Spoiler => "spoiler",
133 Self::Code => "code",
134 Self::Pre { .. } => "pre",
135 Self::TextLink { .. } => "text_link",
136 Self::TextMention { .. } => "text_mention",
137 Self::CustomEmoji { .. } => "custom_emoji",
138 Self::Blockquote => "blockquote",
139 Self::ExpandableBlockquote => "expandable_blockquote",
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct Entity {
151 pub kind: EntityKind,
153 pub offset: usize,
155 pub length: usize,
157}
158
159impl Entity {
160 #[must_use]
162 pub const fn new(kind: EntityKind, offset: usize, length: usize) -> Self {
163 Self {
164 kind,
165 offset,
166 length,
167 }
168 }
169
170 #[must_use]
172 pub const fn end(&self) -> usize {
173 self.offset + self.length
174 }
175}
176
177impl Serialize for Entity {
178 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
179 let extra_fields: usize = match &self.kind {
182 EntityKind::Pre { language } => usize::from(language.is_some()),
183 EntityKind::TextLink { .. }
184 | EntityKind::TextMention { .. }
185 | EntityKind::CustomEmoji { .. } => 1,
186 _ => 0,
187 };
188
189 let mut map: S::SerializeMap = serializer.serialize_map(Some(3 + extra_fields))?;
190 map.serialize_entry("type", self.kind.wire_name())?;
191 map.serialize_entry("offset", &self.offset)?;
192 map.serialize_entry("length", &self.length)?;
193
194 match &self.kind {
195 EntityKind::Pre {
196 language: Some(language),
197 } => map.serialize_entry("language", language)?,
198 EntityKind::TextLink { url } => map.serialize_entry("url", url)?,
199 EntityKind::TextMention { user_id } => {
200 map.serialize_entry("user", &TextMentionUser { id: *user_id })?;
201 }
202 EntityKind::CustomEmoji { custom_emoji_id } => {
203 map.serialize_entry("custom_emoji_id", custom_emoji_id)?;
204 }
205 _ => {}
206 }
207
208 map.end()
209 }
210}
211
212#[derive(Serialize)]
214struct TextMentionUser {
215 id: i64,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::{Entity, EntityKind, Style};
221
222 #[test]
223 fn combined_styles_contain_each_component() {
224 let style: Style = Style::BOLD | Style::ITALIC;
225
226 assert!(style.contains(Style::BOLD));
227 assert!(style.contains(Style::ITALIC));
228 assert!(!style.contains(Style::CODE));
229 }
230
231 #[test]
232 fn empty_style_contains_nothing() {
233 let style: Style = Style::NONE;
234
235 assert!(style.is_empty());
236 assert!(!style.contains(Style::BOLD));
237 }
238
239 #[test]
240 fn combined_styles_expand_to_one_kind_each() {
241 let expected: Vec<EntityKind> = vec![EntityKind::Bold, EntityKind::Italic];
242 let actual: Vec<EntityKind> = (Style::BOLD | Style::ITALIC).kinds();
243 assert_eq!(expected, actual);
244 }
245
246 #[test]
247 fn every_style_flag_expands() {
248 let all: Style = Style::BOLD
249 | Style::ITALIC
250 | Style::UNDERLINE
251 | Style::STRIKETHROUGH
252 | Style::SPOILER
253 | Style::CODE;
254
255 let expected: usize = 6;
256 let actual: usize = all.kinds().len();
257 assert_eq!(expected, actual);
258 }
259
260 #[test]
261 fn entity_end_is_offset_plus_length() {
262 let entity: Entity = Entity::new(EntityKind::Bold, 5, 3);
263
264 let expected: usize = 8;
265 let actual: usize = entity.end();
266 assert_eq!(expected, actual);
267 }
268
269 #[test]
270 fn simple_entity_serializes_flat() {
271 let expected: String = String::from(r#"{"type":"bold","offset":0,"length":4}"#);
272 let actual: String = serde_json::to_string(&Entity::new(EntityKind::Bold, 0, 4))
273 .expect("entity should serialize");
274 assert_eq!(expected, actual);
275 }
276
277 #[test]
278 fn text_link_serializes_its_url() {
279 let entity: Entity = Entity::new(
280 EntityKind::TextLink {
281 url: String::from("https://example.com"),
282 },
283 2,
284 4,
285 );
286
287 let expected: String = String::from(
288 r#"{"type":"text_link","offset":2,"length":4,"url":"https://example.com"}"#,
289 );
290 let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
291 assert_eq!(expected, actual);
292 }
293
294 #[test]
295 fn pre_omits_language_when_absent() {
296 let entity: Entity = Entity::new(EntityKind::Pre { language: None }, 0, 5);
297
298 let expected: String = String::from(r#"{"type":"pre","offset":0,"length":5}"#);
299 let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
300 assert_eq!(expected, actual);
301 }
302
303 #[test]
304 fn pre_includes_language_when_present() {
305 let entity: Entity = Entity::new(
306 EntityKind::Pre {
307 language: Some(String::from("rust")),
308 },
309 0,
310 5,
311 );
312
313 let expected: String =
314 String::from(r#"{"type":"pre","offset":0,"length":5,"language":"rust"}"#);
315 let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
316 assert_eq!(expected, actual);
317 }
318
319 #[test]
320 fn text_mention_serializes_a_nested_user() {
321 let entity: Entity = Entity::new(EntityKind::TextMention { user_id: 42 }, 0, 3);
322
323 let expected: String =
324 String::from(r#"{"type":"text_mention","offset":0,"length":3,"user":{"id":42}}"#);
325 let actual: String = serde_json::to_string(&entity).expect("entity should serialize");
326 assert_eq!(expected, actual);
327 }
328}