1use crate::timestamp::{Timestamp, TimestampStyle};
2
3use super::{MentionIter, MentionType, ParseMentionError, ParseMentionErrorType};
4use crate::fmt::CommandMention;
5use std::{num::NonZeroU64, str::Chars};
6use twilight_model::id::marker::CommandMarker;
7use twilight_model::id::{
8 marker::{ChannelMarker, EmojiMarker, RoleMarker, UserMarker},
9 Id,
10};
11
12pub trait ParseMention: private::Sealed {
21 const SIGILS: &'static [&'static str];
26
27 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
59 where
60 Self: Sized;
61
62 #[must_use = "you must use the iterator to lazily parse mentions"]
70 fn iter(buf: &str) -> MentionIter<'_, Self>
71 where
72 Self: Sized,
73 {
74 MentionIter::new(buf)
75 }
76}
77
78impl ParseMention for Id<ChannelMarker> {
79 const SIGILS: &'static [&'static str] = &["#"];
80
81 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
82 where
83 Self: Sized,
84 {
85 parse_mention(buf, Self::SIGILS).map(|(id, _, _)| Id::from(id))
86 }
87}
88
89impl ParseMention for CommandMention {
90 const SIGILS: &'static [&'static str] = &["/"];
91
92 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
93 where
94 Self: Sized,
95 {
96 let mut echars = buf.chars().enumerate();
99
100 let c = echars.next();
101 if c.map_or(true, |(_, c)| c != '<') {
102 return Err(ParseMentionError {
103 kind: ParseMentionErrorType::LeadingArrow {
104 found: c.map(|(_, c)| c),
105 },
106 source: None,
107 });
108 }
109
110 let c = echars.next();
111 if c.map_or(true, |(_, c)| c != '/') {
112 return Err(ParseMentionError {
113 kind: ParseMentionErrorType::Sigil {
114 expected: Self::SIGILS,
115 found: c.map(|(_, c)| c),
116 },
117 source: None,
118 });
119 }
120
121 let mut segments: Vec<&str> = Vec::new();
122 let mut current_segment: usize = 2;
123 let id_sep = loop {
124 match echars.next() {
125 None => {
126 if !&buf[current_segment..].trim().is_empty() {
127 segments.push(&buf[current_segment..]);
128 }
129
130 let (expected, found) = match segments.len() {
131 0 => (2, 0),
133 1 => (2, 1),
135 2 => (3, 2),
137 3 => (4, 3),
139 _ => {
141 return Err(ParseMentionError {
142 kind: ParseMentionErrorType::ExtraneousPart {
143 found: &buf[current_segment..],
144 },
145 source: None,
146 })
147 }
148 };
149
150 return Err(ParseMentionError {
151 kind: ParseMentionErrorType::PartMissing { expected, found },
152 source: None,
153 });
154 }
155
156 Some((i, ':')) => {
157 if !&buf[current_segment..i].trim().is_empty() {
158 segments.push(&buf[current_segment..i]);
159 }
160 break i;
161 }
162
163 Some((i, ' ')) => {
164 if !buf[current_segment..i].trim().is_empty() {
165 segments.push(&buf[current_segment..i]);
166 }
167
168 current_segment = i + 1;
169 }
170
171 Some(_) => continue,
172 }
173 };
174
175 let id = loop {
176 match echars.next() {
177 None => {
178 return Err(ParseMentionError {
179 kind: ParseMentionErrorType::TrailingArrow { found: None },
180 source: None,
181 })
182 }
183 Some((i, '>')) => break &buf[(id_sep + 1)..i],
184 Some((_, c)) if !c.is_numeric() => {
185 return Err(ParseMentionError {
186 kind: ParseMentionErrorType::TrailingArrow { found: Some(c) },
187 source: None,
188 })
189 }
190 _ => continue,
191 }
192 };
193
194 let id: Id<CommandMarker> = match id.parse() {
195 Ok(id) => id,
196 Err(e) => {
197 return Err(ParseMentionError {
198 kind: ParseMentionErrorType::IdNotU64 { found: id },
199 source: Some(Box::new(e)),
200 })
201 }
202 };
203
204 let mut segments = segments.into_iter();
205 match_command_mention_from_segments(
206 id,
207 segments.next(),
208 segments.next(),
209 segments.next(),
210 segments.next(),
211 )
212 }
213}
214
215impl ParseMention for Id<EmojiMarker> {
216 const SIGILS: &'static [&'static str] = &[":"];
217
218 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
219 where
220 Self: Sized,
221 {
222 parse_mention(buf, Self::SIGILS).map(|(id, _, _)| Id::from(id))
223 }
224}
225
226impl ParseMention for MentionType {
227 const SIGILS: &'static [&'static str] = &["#", ":", "@&", "@", "t:"];
231
232 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
241 where
242 Self: Sized,
243 {
244 let (id, maybe_modifier, found) = parse_mention(buf, Self::SIGILS)?;
245
246 for sigil in Id::<ChannelMarker>::SIGILS {
247 if *sigil == found {
248 return Ok(MentionType::Channel(Id::from(id)));
249 }
250 }
251
252 for sigil in Id::<EmojiMarker>::SIGILS {
253 if *sigil == found {
254 return Ok(MentionType::Emoji(Id::from(id)));
255 }
256 }
257
258 for sigil in Id::<RoleMarker>::SIGILS {
259 if *sigil == found {
260 return Ok(MentionType::Role(Id::from(id)));
261 }
262 }
263
264 for sigil in Timestamp::SIGILS {
265 if *sigil == found {
266 let maybe_style = parse_maybe_style(maybe_modifier)?;
267
268 return Ok(MentionType::Timestamp(Timestamp::new(
269 id.get(),
270 maybe_style,
271 )));
272 }
273 }
274
275 for sigil in Id::<UserMarker>::SIGILS {
276 if *sigil == found {
277 return Ok(MentionType::User(Id::from(id)));
278 }
279 }
280
281 unreachable!("mention type must have been found");
282 }
283}
284
285impl ParseMention for Id<RoleMarker> {
286 const SIGILS: &'static [&'static str] = &["@&"];
287
288 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
289 where
290 Self: Sized,
291 {
292 parse_mention(buf, Self::SIGILS).map(|(id, _, _)| Id::from(id))
293 }
294}
295
296impl ParseMention for Timestamp {
297 const SIGILS: &'static [&'static str] = &["t:"];
298
299 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
308 where
309 Self: Sized,
310 {
311 let (unix, maybe_modifier, _) = parse_mention(buf, Self::SIGILS)?;
312
313 Ok(Timestamp::new(
314 unix.get(),
315 parse_maybe_style(maybe_modifier)?,
316 ))
317 }
318}
319
320impl ParseMention for Id<UserMarker> {
321 const SIGILS: &'static [&'static str] = &["@"];
323
324 fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
325 where
326 Self: Sized,
327 {
328 parse_mention(buf, Self::SIGILS).map(|(id, _, _)| Id::from(id))
329 }
330}
331
332#[inline]
334fn match_command_mention_from_segments<'s>(
335 id: Id<CommandMarker>,
336 first: Option<&'s str>,
337 second: Option<&'s str>,
338 third: Option<&'s str>,
339 fourth: Option<&'s str>,
340) -> Result<CommandMention, ParseMentionError<'s>> {
341 match (first, second, third, fourth) {
342 (_, _, _, Some(extra)) => Err(ParseMentionError {
343 kind: ParseMentionErrorType::ExtraneousPart { found: extra },
344 source: None,
345 }),
346 (None, _, _, _) => {
347 Err(ParseMentionError {
348 kind: ParseMentionErrorType::PartMissing {
349 expected: 2,
351 found: 1,
353 },
354 source: None,
355 })
356 }
357 (Some(name), None, None, None) => Ok(CommandMention::Command {
358 name: name.to_owned(),
359 id,
360 }),
361 (Some(name), Some(sub_command), None, None) => Ok(CommandMention::SubCommand {
362 name: name.to_owned(),
363 sub_command: sub_command.to_owned(),
364 id,
365 }),
366 (Some(name), Some(sub_command_group), Some(sub_command), None) => {
367 Ok(CommandMention::SubCommandGroup {
368 name: name.to_owned(),
369 sub_command: sub_command.to_owned(),
370 sub_command_group: sub_command_group.to_owned(),
371 id,
372 })
373 }
374 _ => unreachable!(),
375 }
376}
377
378fn parse_maybe_style(value: Option<&str>) -> Result<Option<TimestampStyle>, ParseMentionError<'_>> {
385 Ok(if let Some(modifier) = value {
386 Some(
387 TimestampStyle::try_from(modifier).map_err(|source| ParseMentionError {
388 kind: ParseMentionErrorType::TimestampStyleInvalid { found: modifier },
389 source: Some(Box::new(source)),
390 })?,
391 )
392 } else {
393 None
394 })
395}
396
397fn parse_mention<'a>(
408 buf: &'a str,
409 sigils: &'a [&'a str],
410) -> Result<(NonZeroU64, Option<&'a str>, &'a str), ParseMentionError<'a>> {
411 let mut chars = buf.chars();
412
413 let c = chars.next();
414
415 if c != Some('<') {
416 return Err(ParseMentionError {
417 kind: ParseMentionErrorType::LeadingArrow { found: c },
418 source: None,
419 });
420 }
421
422 let maybe_sigil = sigils.iter().find(|sigil| {
423 if chars.as_str().starts_with(*sigil) {
424 for _ in 0..sigil.chars().count() {
425 chars.next();
426 }
427
428 return true;
429 }
430
431 false
432 });
433
434 let sigil = if let Some(sigil) = maybe_sigil {
435 *sigil
436 } else {
437 return Err(ParseMentionError {
438 kind: ParseMentionErrorType::Sigil {
439 expected: sigils,
440 found: chars.next(),
441 },
442 source: None,
443 });
444 };
445
446 if sigil == ":" && !separator_sigil_present(&mut chars) {
447 return Err(ParseMentionError {
448 kind: ParseMentionErrorType::PartMissing {
449 found: 1,
450 expected: 2,
451 },
452 source: None,
453 });
454 }
455
456 let end_position = chars
457 .as_str()
458 .find('>')
459 .ok_or_else(|| ParseMentionError::trailing_arrow(None))?;
460 let maybe_split_position = chars.as_str().find(':');
461
462 let end_of_id_position = maybe_split_position.unwrap_or(end_position);
463
464 let remaining = chars
465 .as_str()
466 .get(..end_of_id_position)
467 .ok_or_else(|| ParseMentionError::trailing_arrow(None))?;
468
469 let num = remaining.parse().map_err(|source| ParseMentionError {
470 kind: ParseMentionErrorType::IdNotU64 { found: remaining },
471 source: Some(Box::new(source)),
472 })?;
473
474 let style = maybe_split_position.and_then(|split_position| {
477 chars.next();
478
479 let style_end_position = end_position - 1;
481
482 chars.as_str().get(split_position..style_end_position)
483 });
484
485 Ok((num, style, sigil))
486}
487
488fn separator_sigil_present(chars: &mut Chars<'_>) -> bool {
491 for c in chars {
492 if c == ':' {
493 return true;
494 }
495 }
496
497 false
498}
499
500mod private {
508 use super::super::MentionType;
509 use crate::fmt::CommandMention;
510 use crate::timestamp::Timestamp;
511 use twilight_model::id::{
512 marker::{ChannelMarker, EmojiMarker, RoleMarker, UserMarker},
513 Id,
514 };
515
516 pub trait Sealed {}
517
518 impl Sealed for Id<ChannelMarker> {}
519 impl Sealed for CommandMention {}
520 impl Sealed for Id<EmojiMarker> {}
521 impl Sealed for MentionType {}
522 impl Sealed for Id<RoleMarker> {}
523 impl Sealed for Timestamp {}
524 impl Sealed for Id<UserMarker> {}
525}
526
527#[cfg(test)]
528mod tests {
529 use super::{
530 super::{MentionType, ParseMentionErrorType},
531 private::Sealed,
532 ParseMention,
533 };
534 use crate::fmt::CommandMention;
535 use crate::{
536 parse::ParseMentionError,
537 timestamp::{Timestamp, TimestampStyle},
538 };
539 use static_assertions::assert_impl_all;
540 use twilight_model::id::{
541 marker::{ChannelMarker, EmojiMarker, RoleMarker, UserMarker},
542 Id,
543 };
544
545 assert_impl_all!(Id<ChannelMarker>: ParseMention, Sealed);
546 assert_impl_all!(CommandMention: ParseMention, Sealed);
547 assert_impl_all!(Id<EmojiMarker>: ParseMention, Sealed);
548 assert_impl_all!(MentionType: ParseMention, Sealed);
549 assert_impl_all!(Id<RoleMarker>: ParseMention, Sealed);
550 assert_impl_all!(Id<UserMarker>: ParseMention, Sealed);
551
552 #[test]
553 fn sigils() {
554 assert_eq!(&["#"], Id::<ChannelMarker>::SIGILS);
555 assert_eq!(&["/"], CommandMention::SIGILS);
556 assert_eq!(&[":"], Id::<EmojiMarker>::SIGILS);
557 assert_eq!(&["#", ":", "@&", "@", "t:"], MentionType::SIGILS);
558 assert_eq!(&["@&"], Id::<RoleMarker>::SIGILS);
559 assert_eq!(&["@"], Id::<UserMarker>::SIGILS);
560 }
561
562 #[test]
563 fn parse_channel_id() {
564 assert_eq!(Id::<ChannelMarker>::new(123), Id::parse("<#123>").unwrap());
565 assert_eq!(
566 &ParseMentionErrorType::Sigil {
567 expected: &["#"],
568 found: Some('@'),
569 },
570 Id::<ChannelMarker>::parse("<@123>").unwrap_err().kind(),
571 );
572 }
573
574 #[test]
575 fn parse_command_mention() {
576 assert_eq!(
577 &ParseMentionErrorType::PartMissing {
578 expected: 2,
579 found: 1,
580 },
581 CommandMention::parse("</ :123>").unwrap_err().kind()
582 );
583
584 assert_eq!(
585 CommandMention::Command {
586 name: "command".to_owned(),
587 id: Id::new(123)
588 },
589 CommandMention::parse("</command:123>").unwrap()
590 );
591
592 assert_eq!(
593 CommandMention::SubCommand {
594 name: "command".to_owned(),
595 sub_command: "subcommand".to_owned(),
596 id: Id::new(123)
597 },
598 CommandMention::parse("</command subcommand:123>").unwrap()
599 );
600
601 assert_eq!(
603 CommandMention::SubCommand {
604 name: "command".to_owned(),
605 sub_command: "subcommand".to_owned(),
606 id: Id::new(123)
607 },
608 CommandMention::parse("</command subcommand:123>").unwrap()
609 );
610
611 assert_eq!(
612 CommandMention::SubCommandGroup {
613 name: "command".to_owned(),
614 sub_command: "subcommand".to_owned(),
615 sub_command_group: "subcommand_group".to_owned(),
616 id: Id::new(123)
617 },
618 CommandMention::parse("</command subcommand_group subcommand:123>").unwrap()
619 );
620
621 assert_eq!(
622 &ParseMentionErrorType::ExtraneousPart { found: "d" },
623 CommandMention::parse("</a b c d:123>").unwrap_err().kind()
624 );
625
626 assert_eq!(
627 &ParseMentionErrorType::IdNotU64 { found: "0" },
628 CommandMention::parse("</a:0>").unwrap_err().kind()
629 );
630
631 assert_eq!(
632 &ParseMentionErrorType::TrailingArrow { found: Some('b') },
633 CommandMention::parse("</a:b>").unwrap_err().kind()
634 );
635
636 assert_eq!(
637 &ParseMentionErrorType::TrailingArrow { found: None },
638 CommandMention::parse("</a:123").unwrap_err().kind()
639 );
640
641 for (input, expected, found) in [
642 ("</", 2, 0),
643 ("</a", 2, 1),
644 ("</a b", 3, 2),
645 ("</a b c", 4, 3),
646 ] {
647 assert_eq!(
648 &ParseMentionErrorType::PartMissing { expected, found },
649 CommandMention::parse(input).unwrap_err().kind()
650 );
651 }
652
653 assert_eq!(
654 &ParseMentionErrorType::ExtraneousPart { found: "d" },
655 CommandMention::parse("</a b c d").unwrap_err().kind()
656 );
657 }
658
659 #[test]
660 fn parse_emoji_id() {
661 assert_eq!(
662 Id::<EmojiMarker>::new(123),
663 Id::parse("<:name:123>").unwrap()
664 );
665 assert_eq!(
666 &ParseMentionErrorType::Sigil {
667 expected: &[":"],
668 found: Some('@'),
669 },
670 Id::<EmojiMarker>::parse("<@123>").unwrap_err().kind(),
671 );
672 }
673
674 #[test]
675 fn parse_mention_type() {
676 assert_eq!(
677 MentionType::Channel(Id::new(123)),
678 MentionType::parse("<#123>").unwrap()
679 );
680 assert_eq!(
681 MentionType::Emoji(Id::new(123)),
682 MentionType::parse("<:name:123>").unwrap()
683 );
684 assert_eq!(
685 MentionType::Role(Id::new(123)),
686 MentionType::parse("<@&123>").unwrap()
687 );
688 assert_eq!(
689 MentionType::User(Id::new(123)),
690 MentionType::parse("<@123>").unwrap()
691 );
692 assert_eq!(
693 &ParseMentionErrorType::Sigil {
694 expected: &["#", ":", "@&", "@", "t:"],
695 found: Some(';'),
696 },
697 MentionType::parse("<;123>").unwrap_err().kind(),
698 );
699 }
700
701 #[test]
702 fn parse_role_id() {
703 assert_eq!(Id::<RoleMarker>::new(123), Id::parse("<@&123>").unwrap());
704 assert_eq!(
705 &ParseMentionErrorType::Sigil {
706 expected: &["@&"],
707 found: Some('@'),
708 },
709 Id::<RoleMarker>::parse("<@123>").unwrap_err().kind(),
710 );
711 }
712
713 #[test]
714 fn parse_timestamp() -> Result<(), ParseMentionError<'static>> {
715 assert_eq!(Timestamp::new(123, None), Timestamp::parse("<t:123>")?);
716 assert_eq!(
717 Timestamp::new(123, Some(TimestampStyle::RelativeTime)),
718 Timestamp::parse("<t:123:R>")?
719 );
720 assert_eq!(
721 &ParseMentionErrorType::TimestampStyleInvalid { found: "?" },
722 Timestamp::parse("<t:123:?>").unwrap_err().kind(),
723 );
724
725 Ok(())
726 }
727
728 #[test]
729 fn parse_user_id() {
730 assert_eq!(Id::<UserMarker>::new(123), Id::parse("<@123>").unwrap());
731 assert_eq!(
732 &ParseMentionErrorType::IdNotU64 { found: "&123" },
733 Id::<UserMarker>::parse("<@&123>").unwrap_err().kind(),
734 );
735 }
736
737 #[test]
738 fn parse_id_wrong_sigil() {
739 assert_eq!(
740 &ParseMentionErrorType::Sigil {
741 expected: &["@"],
742 found: Some('#'),
743 },
744 super::parse_mention("<#123>", &["@"]).unwrap_err().kind(),
745 );
746 assert_eq!(
747 &ParseMentionErrorType::Sigil {
748 expected: &["#"],
749 found: None,
750 },
751 super::parse_mention("<", &["#"]).unwrap_err().kind(),
752 );
753 assert_eq!(
754 &ParseMentionErrorType::Sigil {
755 expected: &["#"],
756 found: None,
757 },
758 super::parse_mention("<", &["#"]).unwrap_err().kind(),
759 );
760 }
761}