1use std::{
2 convert::TryFrom,
3 error::Error,
4 fmt,
5 ops::{Index, IndexMut, Range},
6};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Error as JsonError;
10
11use crate::types::User;
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
18#[serde(into = "Vec<TextEntity>", try_from = "Vec<RawTextEntity>")]
19pub struct TextEntities {
20 items: Vec<TextEntity>,
21}
22
23impl TextEntities {
24 pub fn push(&mut self, value: TextEntity) {
30 self.items.push(value);
31 }
32
33 pub fn serialize(&self) -> Result<String, TextEntityError> {
35 serde_json::to_string(self).map_err(TextEntityError::Serialize)
36 }
37}
38
39impl TryFrom<Vec<RawTextEntity>> for TextEntities {
40 type Error = TextEntityError;
41
42 fn try_from(entities: Vec<RawTextEntity>) -> Result<Self, Self::Error> {
43 entities
44 .into_iter()
45 .map(TryFrom::try_from)
46 .collect::<Result<Vec<TextEntity>, _>>()
47 .map(|items| Self { items })
48 }
49}
50
51impl FromIterator<TextEntity> for TextEntities {
52 fn from_iter<T: IntoIterator<Item = TextEntity>>(iter: T) -> Self {
53 Self {
54 items: iter.into_iter().collect(),
55 }
56 }
57}
58
59impl IntoIterator for TextEntities {
60 type Item = TextEntity;
61 type IntoIter = std::vec::IntoIter<Self::Item>;
62
63 fn into_iter(self) -> Self::IntoIter {
64 self.items.into_iter()
65 }
66}
67
68impl<'a> IntoIterator for &'a TextEntities {
69 type Item = &'a TextEntity;
70 type IntoIter = std::slice::Iter<'a, TextEntity>;
71
72 fn into_iter(self) -> Self::IntoIter {
73 self.items.as_slice().iter()
74 }
75}
76
77impl<'a> IntoIterator for &'a mut TextEntities {
78 type Item = &'a mut TextEntity;
79 type IntoIter = std::slice::IterMut<'a, TextEntity>;
80
81 fn into_iter(self) -> Self::IntoIter {
82 self.items.as_mut_slice().iter_mut()
83 }
84}
85
86impl Index<usize> for TextEntities {
87 type Output = TextEntity;
88
89 fn index(&self, index: usize) -> &Self::Output {
90 &self.items[index]
91 }
92}
93
94impl IndexMut<usize> for TextEntities {
95 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
96 &mut self.items[index]
97 }
98}
99
100impl From<TextEntities> for Vec<TextEntity> {
101 fn from(entities: TextEntities) -> Self {
102 entities.items
103 }
104}
105
106#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
108#[serde(try_from = "RawTextEntity", into = "RawTextEntity")]
109pub enum TextEntity {
110 Blockquote(TextEntityPosition),
112 Bold(TextEntityPosition),
114 BotCommand(TextEntityPosition),
116 Cashtag(TextEntityPosition),
118 Code(TextEntityPosition),
120 CustomEmoji {
122 custom_emoji_id: String,
126 position: TextEntityPosition,
128 },
129 Email(TextEntityPosition),
131 ExpandableBlockquote(TextEntityPosition),
133 Hashtag(TextEntityPosition),
135 Italic(TextEntityPosition),
137 Mention(TextEntityPosition),
139 PhoneNumber(TextEntityPosition),
141 Pre {
143 position: TextEntityPosition,
145 language: Option<String>,
147 },
148 Spoiler(TextEntityPosition),
150 Strikethrough(TextEntityPosition),
152 TextLink {
154 position: TextEntityPosition,
156 url: String,
158 },
159 TextMention {
161 position: TextEntityPosition,
163 user: User,
165 },
166 Underline(TextEntityPosition),
168 Url(TextEntityPosition),
170}
171
172macro_rules! text_entity_factory {
173 ($($method_name:ident => $enum_variant: ident),*) => {
174 $(
175 pub fn $method_name<T: Into<TextEntityPosition>>(pos: T) -> TextEntity {
181 TextEntity::$enum_variant(pos.into())
182 }
183 )*
184 };
185}
186
187impl TextEntity {
188 text_entity_factory!(
189 blockquote => Blockquote,
190 bold => Bold,
191 bot_command => BotCommand,
192 cashtag => Cashtag,
193 code => Code,
194 email => Email,
195 expandable_blockquote => ExpandableBlockquote,
196 hashtag => Hashtag,
197 italic => Italic,
198 mention => Mention,
199 phone_number => PhoneNumber,
200 spoiler => Spoiler,
201 strikethrough => Strikethrough,
202 underline => Underline
203 );
204
205 pub fn custom_emoji<P: Into<TextEntityPosition>, I: Into<String>>(pos: P, custom_emoji_id: I) -> TextEntity {
212 TextEntity::CustomEmoji {
213 position: pos.into(),
214 custom_emoji_id: custom_emoji_id.into(),
215 }
216 }
217
218 pub fn pre<P: Into<TextEntityPosition>, L: Into<String>>(pos: P, language: Option<L>) -> TextEntity {
225 TextEntity::Pre {
226 position: pos.into(),
227 language: language.map(|x| x.into()),
228 }
229 }
230
231 pub fn text_link<P: Into<TextEntityPosition>, U: Into<String>>(pos: P, url: U) -> TextEntity {
238 TextEntity::TextLink {
239 position: pos.into(),
240 url: url.into(),
241 }
242 }
243
244 pub fn text_mention<P: Into<TextEntityPosition>>(pos: P, user: User) -> TextEntity {
251 TextEntity::TextMention {
252 position: pos.into(),
253 user,
254 }
255 }
256}
257
258#[derive(Clone, Debug, Deserialize, Serialize)]
259struct RawTextEntity {
260 offset: u32,
261 length: u32,
262 #[serde(flatten)]
263 entity_type: RawTextEntityType,
264}
265
266#[serde_with::skip_serializing_none]
267#[derive(Clone, Debug, Deserialize, Serialize)]
268#[serde(rename_all = "snake_case")]
269#[serde(tag = "type")]
270enum RawTextEntityType {
271 Blockquote,
272 Bold,
273 BotCommand,
274 Cashtag,
275 Code,
276 CustomEmoji { custom_emoji_id: Option<String> },
277 Email,
278 ExpandableBlockquote,
279 Hashtag,
280 Italic,
281 Mention,
282 PhoneNumber,
283 Pre { language: Option<String> },
284 Spoiler,
285 Strikethrough,
286 TextLink { url: Option<String> },
287 TextMention { user: Option<User> },
288 Underline,
289 Url,
290}
291
292#[derive(Debug)]
294pub enum TextEntityError {
295 NoCustomEmoji,
297 NoUrl,
299 NoUser,
301 Serialize(JsonError),
303}
304
305impl Error for TextEntityError {
306 fn source(&self) -> Option<&(dyn Error + 'static)> {
307 match self {
308 Self::Serialize(err) => Some(err),
309 _ => None,
310 }
311 }
312}
313
314impl fmt::Display for TextEntityError {
315 fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
316 use self::TextEntityError::*;
317 write!(
318 out,
319 "{}",
320 match self {
321 NoCustomEmoji => String::from("Custom emoji is required for custom_emoji entity"),
322 NoUrl => String::from("URL is required for text_link entity"),
323 NoUser => String::from("user is required for text_mention entity"),
324 Serialize(err) => format!("failed to serialize text entities: {}", err),
325 }
326 )
327 }
328}
329
330impl TryFrom<RawTextEntity> for TextEntity {
331 type Error = TextEntityError;
332
333 fn try_from(raw: RawTextEntity) -> Result<Self, Self::Error> {
334 let position = TextEntityPosition {
335 offset: raw.offset,
336 length: raw.length,
337 };
338
339 Ok(match raw.entity_type {
340 RawTextEntityType::Blockquote => TextEntity::Blockquote(position),
341 RawTextEntityType::Bold => TextEntity::Bold(position),
342 RawTextEntityType::BotCommand => TextEntity::BotCommand(position),
343 RawTextEntityType::Cashtag => TextEntity::Cashtag(position),
344 RawTextEntityType::Code => TextEntity::Code(position),
345 RawTextEntityType::CustomEmoji { custom_emoji_id } => TextEntity::CustomEmoji {
346 position,
347 custom_emoji_id: custom_emoji_id.ok_or(TextEntityError::NoCustomEmoji)?,
348 },
349 RawTextEntityType::Email => TextEntity::Email(position),
350 RawTextEntityType::ExpandableBlockquote => TextEntity::ExpandableBlockquote(position),
351 RawTextEntityType::Hashtag => TextEntity::Hashtag(position),
352 RawTextEntityType::Italic => TextEntity::Italic(position),
353 RawTextEntityType::Mention => TextEntity::Mention(position),
354 RawTextEntityType::PhoneNumber => TextEntity::PhoneNumber(position),
355 RawTextEntityType::Pre { language } => TextEntity::Pre { position, language },
356 RawTextEntityType::Spoiler => TextEntity::Spoiler(position),
357 RawTextEntityType::Strikethrough => TextEntity::Strikethrough(position),
358 RawTextEntityType::TextLink { url } => TextEntity::TextLink {
359 position,
360 url: url.ok_or(TextEntityError::NoUrl)?,
361 },
362 RawTextEntityType::TextMention { user } => TextEntity::TextMention {
363 position,
364 user: user.ok_or(TextEntityError::NoUser)?,
365 },
366 RawTextEntityType::Underline => TextEntity::Underline(position),
367 RawTextEntityType::Url => TextEntity::Url(position),
368 })
369 }
370}
371
372impl From<TextEntity> for RawTextEntity {
373 fn from(entity: TextEntity) -> Self {
374 macro_rules! raw {
375 ($entity_type:ident($position:ident $( , $item:ident )?)) => {
376 RawTextEntity {
377 entity_type: RawTextEntityType::$entity_type $( { $item: $item.into() } )?,
378 offset: $position.offset as _,
379 length: $position.length as _,
380 }
381 };
382 }
383 match entity {
384 TextEntity::Blockquote(p) => raw!(Blockquote(p)),
385 TextEntity::Bold(p) => raw!(Bold(p)),
386 TextEntity::BotCommand(p) => raw!(BotCommand(p)),
387 TextEntity::Cashtag(p) => raw!(Cashtag(p)),
388 TextEntity::Code(p) => raw!(Code(p)),
389 TextEntity::CustomEmoji {
390 position: p,
391 custom_emoji_id,
392 } => raw!(CustomEmoji(p, custom_emoji_id)),
393 TextEntity::Email(p) => raw!(Email(p)),
394 TextEntity::ExpandableBlockquote(p) => raw!(ExpandableBlockquote(p)),
395 TextEntity::Hashtag(p) => raw!(Hashtag(p)),
396 TextEntity::Italic(p) => raw!(Italic(p)),
397 TextEntity::Mention(p) => raw!(Mention(p)),
398 TextEntity::PhoneNumber(p) => raw!(PhoneNumber(p)),
399 TextEntity::Pre { position: p, language } => raw!(Pre(p, language)),
400 TextEntity::Spoiler(p) => raw!(Spoiler(p)),
401 TextEntity::Strikethrough(p) => raw!(Strikethrough(p)),
402 TextEntity::TextLink { position: p, url } => raw!(TextLink(p, url)),
403 TextEntity::TextMention { position: p, user } => raw!(TextMention(p, user)),
404 TextEntity::Underline(p) => raw!(Underline(p)),
405 TextEntity::Url(p) => raw!(Url(p)),
406 }
407 }
408}
409
410#[derive(Clone, Debug, PartialEq, PartialOrd)]
414pub struct TextEntityBotCommand {
415 pub command: String,
417 pub bot_name: Option<String>,
419}
420
421#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
423pub struct TextEntityPosition {
424 pub offset: u32,
426 pub length: u32,
428}
429
430impl From<Range<u32>> for TextEntityPosition {
431 fn from(range: Range<u32>) -> Self {
432 Self {
433 offset: range.start,
434 length: range.end - range.start,
435 }
436 }
437}