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::{Integer, User};
12
13#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
15#[serde(into = "Vec<TextEntity>", try_from = "Vec<RawTextEntity>")]
16pub struct TextEntities {
17 items: Vec<TextEntity>,
18}
19
20impl TextEntities {
21 pub fn push(&mut self, value: TextEntity) {
27 self.items.push(value);
28 }
29
30 pub fn serialize(&self) -> Result<String, TextEntityError> {
32 serde_json::to_string(self).map_err(TextEntityError::Serialize)
33 }
34}
35
36impl TryFrom<Vec<RawTextEntity>> for TextEntities {
37 type Error = TextEntityError;
38
39 fn try_from(entities: Vec<RawTextEntity>) -> Result<Self, Self::Error> {
40 entities
41 .into_iter()
42 .map(TryFrom::try_from)
43 .collect::<Result<Vec<TextEntity>, _>>()
44 .map(|items| Self { items })
45 }
46}
47
48impl FromIterator<TextEntity> for TextEntities {
49 fn from_iter<T: IntoIterator<Item = TextEntity>>(iter: T) -> Self {
50 Self {
51 items: iter.into_iter().collect(),
52 }
53 }
54}
55
56impl IntoIterator for TextEntities {
57 type Item = TextEntity;
58 type IntoIter = std::vec::IntoIter<Self::Item>;
59
60 fn into_iter(self) -> Self::IntoIter {
61 self.items.into_iter()
62 }
63}
64
65impl<'a> IntoIterator for &'a TextEntities {
66 type Item = &'a TextEntity;
67 type IntoIter = std::slice::Iter<'a, TextEntity>;
68
69 fn into_iter(self) -> Self::IntoIter {
70 self.items.as_slice().iter()
71 }
72}
73
74impl<'a> IntoIterator for &'a mut TextEntities {
75 type Item = &'a mut TextEntity;
76 type IntoIter = std::slice::IterMut<'a, TextEntity>;
77
78 fn into_iter(self) -> Self::IntoIter {
79 self.items.as_mut_slice().iter_mut()
80 }
81}
82
83impl Index<usize> for TextEntities {
84 type Output = TextEntity;
85
86 fn index(&self, index: usize) -> &Self::Output {
87 &self.items[index]
88 }
89}
90
91impl IndexMut<usize> for TextEntities {
92 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
93 &mut self.items[index]
94 }
95}
96
97impl From<TextEntities> for Vec<TextEntity> {
98 fn from(entities: TextEntities) -> Self {
99 entities.items
100 }
101}
102
103#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
105#[serde(try_from = "RawTextEntity", into = "RawTextEntity")]
106pub enum TextEntity {
107 Blockquote(TextEntityPosition),
109 Bold(TextEntityPosition),
111 BotCommand(TextEntityPosition),
113 Cashtag(TextEntityPosition),
115 Code(TextEntityPosition),
117 CustomEmoji {
119 custom_emoji_id: String,
123 position: TextEntityPosition,
125 },
126 DateTime {
128 position: TextEntityPosition,
130 unix_time: Option<Integer>,
132 format: Option<String>,
138 },
139 Email(TextEntityPosition),
141 ExpandableBlockquote(TextEntityPosition),
143 Hashtag(TextEntityPosition),
145 Italic(TextEntityPosition),
147 Mention(TextEntityPosition),
149 PhoneNumber(TextEntityPosition),
151 Pre {
153 position: TextEntityPosition,
155 language: Option<String>,
157 },
158 Spoiler(TextEntityPosition),
160 Strikethrough(TextEntityPosition),
162 TextLink {
164 position: TextEntityPosition,
166 url: String,
168 },
169 TextMention {
171 position: TextEntityPosition,
173 user: User,
175 },
176 Underline(TextEntityPosition),
178 Url(TextEntityPosition),
180}
181
182macro_rules! text_entity_factory {
183 ($($method_name:ident => $enum_variant: ident),*) => {
184 $(
185 pub fn $method_name<T: Into<TextEntityPosition>>(pos: T) -> Self {
191 Self::$enum_variant(pos.into())
192 }
193 )*
194 };
195}
196
197impl TextEntity {
198 text_entity_factory!(
199 blockquote => Blockquote,
200 bold => Bold,
201 bot_command => BotCommand,
202 cashtag => Cashtag,
203 code => Code,
204 email => Email,
205 expandable_blockquote => ExpandableBlockquote,
206 hashtag => Hashtag,
207 italic => Italic,
208 mention => Mention,
209 phone_number => PhoneNumber,
210 spoiler => Spoiler,
211 strikethrough => Strikethrough,
212 underline => Underline
213 );
214
215 pub fn custom_emoji<A, B>(pos: A, custom_emoji_id: B) -> Self
222 where
223 A: Into<TextEntityPosition>,
224 B: Into<String>,
225 {
226 Self::CustomEmoji {
227 position: pos.into(),
228 custom_emoji_id: custom_emoji_id.into(),
229 }
230 }
231
232 pub fn date_time<A, B>(pos: A, unix_time: Option<Integer>, format: Option<B>) -> Self
240 where
241 A: Into<TextEntityPosition>,
242 B: Into<String>,
243 {
244 Self::DateTime {
245 position: pos.into(),
246 unix_time,
247 format: format.map(Into::into),
248 }
249 }
250
251 pub fn pre<A, B>(pos: A, language: Option<B>) -> Self
258 where
259 A: Into<TextEntityPosition>,
260 B: Into<String>,
261 {
262 Self::Pre {
263 position: pos.into(),
264 language: language.map(|x| x.into()),
265 }
266 }
267
268 pub fn text_link<A, B>(pos: A, url: B) -> Self
275 where
276 A: Into<TextEntityPosition>,
277 B: Into<String>,
278 {
279 Self::TextLink {
280 position: pos.into(),
281 url: url.into(),
282 }
283 }
284
285 pub fn text_mention<T>(pos: T, user: User) -> Self
292 where
293 T: Into<TextEntityPosition>,
294 {
295 Self::TextMention {
296 position: pos.into(),
297 user,
298 }
299 }
300}
301
302#[derive(Clone, Debug, Deserialize, Serialize)]
303struct RawTextEntity {
304 offset: u32,
305 length: u32,
306 #[serde(flatten)]
307 entity_type: RawTextEntityType,
308}
309
310#[serde_with::skip_serializing_none]
311#[derive(Clone, Debug, Deserialize, Serialize)]
312#[serde(rename_all = "snake_case")]
313#[serde(tag = "type")]
314enum RawTextEntityType {
315 Blockquote,
316 Bold,
317 BotCommand,
318 Cashtag,
319 Code,
320 CustomEmoji {
321 custom_emoji_id: Option<String>,
322 },
323 DateTime {
324 date_time_format: Option<String>,
325 unix_time: Option<Integer>,
326 },
327 Email,
328 ExpandableBlockquote,
329 Hashtag,
330 Italic,
331 Mention,
332 PhoneNumber,
333 Pre {
334 language: Option<String>,
335 },
336 Spoiler,
337 Strikethrough,
338 TextLink {
339 url: Option<String>,
340 },
341 TextMention {
342 user: Option<User>,
343 },
344 Underline,
345 Url,
346}
347
348#[derive(Debug)]
350pub enum TextEntityError {
351 NoCustomEmoji,
353 NoUrl,
355 NoUser,
357 Serialize(JsonError),
359}
360
361impl Error for TextEntityError {
362 fn source(&self) -> Option<&(dyn Error + 'static)> {
363 match self {
364 Self::Serialize(err) => Some(err),
365 _ => None,
366 }
367 }
368}
369
370impl fmt::Display for TextEntityError {
371 fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
372 use self::TextEntityError::*;
373 write!(
374 out,
375 "{}",
376 match self {
377 NoCustomEmoji => String::from("Custom emoji is required for custom_emoji entity"),
378 NoUrl => String::from("URL is required for text_link entity"),
379 NoUser => String::from("user is required for text_mention entity"),
380 Serialize(err) => format!("failed to serialize text entities: {err}"),
381 }
382 )
383 }
384}
385
386impl TryFrom<RawTextEntity> for TextEntity {
387 type Error = TextEntityError;
388
389 fn try_from(raw: RawTextEntity) -> Result<Self, Self::Error> {
390 let position = TextEntityPosition {
391 offset: raw.offset,
392 length: raw.length,
393 };
394
395 Ok(match raw.entity_type {
396 RawTextEntityType::Blockquote => Self::Blockquote(position),
397 RawTextEntityType::Bold => Self::Bold(position),
398 RawTextEntityType::BotCommand => Self::BotCommand(position),
399 RawTextEntityType::Cashtag => Self::Cashtag(position),
400 RawTextEntityType::Code => Self::Code(position),
401 RawTextEntityType::CustomEmoji { custom_emoji_id } => Self::CustomEmoji {
402 position,
403 custom_emoji_id: custom_emoji_id.ok_or(TextEntityError::NoCustomEmoji)?,
404 },
405 RawTextEntityType::DateTime {
406 unix_time,
407 date_time_format,
408 } => Self::DateTime {
409 position,
410 unix_time,
411 format: date_time_format,
412 },
413 RawTextEntityType::Email => Self::Email(position),
414 RawTextEntityType::ExpandableBlockquote => Self::ExpandableBlockquote(position),
415 RawTextEntityType::Hashtag => Self::Hashtag(position),
416 RawTextEntityType::Italic => Self::Italic(position),
417 RawTextEntityType::Mention => Self::Mention(position),
418 RawTextEntityType::PhoneNumber => Self::PhoneNumber(position),
419 RawTextEntityType::Pre { language } => Self::Pre { position, language },
420 RawTextEntityType::Spoiler => Self::Spoiler(position),
421 RawTextEntityType::Strikethrough => Self::Strikethrough(position),
422 RawTextEntityType::TextLink { url } => Self::TextLink {
423 position,
424 url: url.ok_or(TextEntityError::NoUrl)?,
425 },
426 RawTextEntityType::TextMention { user } => Self::TextMention {
427 position,
428 user: user.ok_or(TextEntityError::NoUser)?,
429 },
430 RawTextEntityType::Underline => Self::Underline(position),
431 RawTextEntityType::Url => Self::Url(position),
432 })
433 }
434}
435
436impl From<TextEntity> for RawTextEntity {
437 fn from(entity: TextEntity) -> Self {
438 macro_rules! raw {
439 ($entity_type:ident($position:ident $( $($item:ident)+ )?)) => {
440 Self {
441 entity_type: RawTextEntityType::$entity_type $( { $($item: $item.into(),)+ } )?,
442 offset: $position.offset as _,
443 length: $position.length as _,
444 }
445 };
446 }
447 match entity {
448 TextEntity::Blockquote(p) => raw!(Blockquote(p)),
449 TextEntity::Bold(p) => raw!(Bold(p)),
450 TextEntity::BotCommand(p) => raw!(BotCommand(p)),
451 TextEntity::Cashtag(p) => raw!(Cashtag(p)),
452 TextEntity::Code(p) => raw!(Code(p)),
453 TextEntity::CustomEmoji {
454 position: p,
455 custom_emoji_id,
456 } => raw!(CustomEmoji(p custom_emoji_id)),
457 TextEntity::DateTime {
458 position: p,
459 unix_time,
460 format: date_time_format,
461 } => raw!(DateTime(p unix_time date_time_format)),
462 TextEntity::Email(p) => raw!(Email(p)),
463 TextEntity::ExpandableBlockquote(p) => raw!(ExpandableBlockquote(p)),
464 TextEntity::Hashtag(p) => raw!(Hashtag(p)),
465 TextEntity::Italic(p) => raw!(Italic(p)),
466 TextEntity::Mention(p) => raw!(Mention(p)),
467 TextEntity::PhoneNumber(p) => raw!(PhoneNumber(p)),
468 TextEntity::Pre { position: p, language } => raw!(Pre(p language)),
469 TextEntity::Spoiler(p) => raw!(Spoiler(p)),
470 TextEntity::Strikethrough(p) => raw!(Strikethrough(p)),
471 TextEntity::TextLink { position: p, url } => raw!(TextLink(p url)),
472 TextEntity::TextMention { position: p, user } => raw!(TextMention(p user)),
473 TextEntity::Underline(p) => raw!(Underline(p)),
474 TextEntity::Url(p) => raw!(Url(p)),
475 }
476 }
477}
478
479#[derive(Clone, Debug, PartialEq, PartialOrd)]
483pub struct TextEntityBotCommand {
484 pub command: String,
486 pub bot_name: Option<String>,
488}
489
490#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
492pub struct TextEntityPosition {
493 pub offset: u32,
495 pub length: u32,
497}
498
499impl From<Range<u32>> for TextEntityPosition {
500 fn from(range: Range<u32>) -> Self {
501 Self {
502 offset: range.start,
503 length: range.end - range.start,
504 }
505 }
506}