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