twilight_mention/parse/
impl.rs

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
11/// Parse mentions out of buffers.
12///
13/// While the syntax of mentions will be validated and the IDs within them
14/// parsed, they won't be validated as being proper snowflakes or as real IDs in
15/// use.
16///
17/// **Note** that this trait is sealed and is not meant to be manually
18/// implemented.
19pub trait ParseMention: private::Sealed {
20    /// Leading sigil(s) of the mention after the leading arrow (`<`).
21    ///
22    /// In a channel mention, the sigil is `#`. In the case of a user mention,
23    /// the sigil would be `@`.
24    const SIGILS: &'static [&'static str];
25
26    /// Parse a mention out of a buffer.
27    ///
28    /// This will not search the buffer for a mention and will instead treat the
29    /// entire buffer as a mention.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use twilight_mention::ParseMention;
35    /// use twilight_model::id::{
36    ///     Id,
37    ///     marker::{ChannelMarker, UserMarker},
38    /// };
39    ///
40    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
41    /// assert_eq!(Id::<ChannelMarker>::new(123), Id::parse("<#123>")?,);
42    /// assert_eq!(Id::<UserMarker>::new(456), Id::parse("<@456>")?,);
43    /// assert!(Id::<ChannelMarker>::parse("not a mention").is_err());
44    /// # Ok(()) }
45    /// ```
46    ///
47    /// # Errors
48    ///
49    /// Returns a [`ParseMentionErrorType::LeadingArrow`] error type if the
50    /// leading arrow is not present.
51    ///
52    /// Returns a [`ParseMentionErrorType::Sigil`] error type if the mention
53    /// type's sigil is not present after the leading arrow.
54    ///
55    /// Returns a [`ParseMentionErrorType::TrailingArrow`] error type if the
56    /// trailing arrow is not present after the ID.
57    fn parse(buf: &str) -> Result<Self, ParseMentionError<'_>>
58    where
59        Self: Sized;
60
61    /// Search a buffer for mentions and parse out any that are encountered.
62    ///
63    /// Unlike [`parse`], this will not error if anything that is indicative of
64    /// a mention is encountered but did not successfully parse, such as a `<`
65    /// but with no trailing mention sigil.
66    ///
67    /// [`parse`]: Self::parse
68    #[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        // Can't use `parse_mention` due to significant pattern differences for command mentions.
96
97        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                        // no segment found, require name and id
131                        0 => (2, 0),
132                        // only found name, needs id
133                        1 => (2, 1),
134                        // only found name and subcommand, needs id
135                        2 => (3, 2),
136                        // only found name, subcommand and subcommand group, needs id
137                        3 => (4, 3),
138                        // too many segments
139                        _ => {
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    /// Sigils for any type of mention.
227    ///
228    /// Contains all of the sigils of every other type of mention.
229    const SIGILS: &'static [&'static str] = &["#", ":", "@&", "@", "t:"];
230
231    /// Parse a mention from a string slice.
232    ///
233    /// # Examples
234    ///
235    /// Returns [`ParseMentionErrorType::TimestampStyleInvalid`] if a timestamp
236    /// style value is invalid.
237    ///
238    /// [`ParseMentionError::TimestampStyleInvalid`]: super::error::ParseMentionErrorType::TimestampStyleInvalid
239    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    /// Parse a timestamp from a string slice.
299    ///
300    /// # Examples
301    ///
302    /// Returns [`ParseMentionErrorType::TimestampStyleInvalid`] if the
303    /// timestamp style value is invalid.
304    ///
305    /// [`ParseMentionError::TimestampStyleInvalid`]: super::error::ParseMentionErrorType::TimestampStyleInvalid
306    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    /// Sigil for User ID mentions.
321    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/// Matches the four segments of [`CommandMention::parse`] into the final `Result` for it.
332#[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                    // at least two for command
349                    expected: 2,
350                    // we found the id until now
351                    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
377/// Parse a possible style value string slice into a [`TimestampStyle`].
378///
379/// # Errors
380///
381/// Returns [`ParseMentionErrorType::TimestampStyleInvalid`] if the timestamp
382/// style value is invalid.
383fn 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
396/// # Errors
397///
398/// Returns [`ParseMentionErrorType::LeadingArrow`] if the leading arrow is not
399/// present.
400///
401/// Returns [`ParseMentionErrorType::Sigil`] if the mention type's sigil is not
402/// present after the leading arrow.
403///
404/// Returns [`ParseMentionErrorType::TrailingArrow`] if the trailing arrow is
405/// not present after the ID.
406fn 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    // If additional information - like a timestamp style - is present then we
474    // can just get a subslice of the string via the split and ending positions.
475    let style = maybe_split_position.and_then(|split_position| {
476        chars.next();
477
478        // We need to remove 1 so we don't catch the `>` in it.
479        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
487// Don't use `Iterator::skip_while` so we can mutate `chars` in-place;
488// `skip_while` is consuming.
489fn 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
499/// Rust doesn't allow leaking private implementations, but if we make the trait
500/// public in a private scope then it gets by the restriction and doesn't allow
501/// Sealed to be named.
502///
503/// Yes, this is the correct way of sealing a trait:
504///
505/// <https://rust-lang.github.io/api-guidelines/future-proofing.html>
506mod 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        // this is more relaxed than the discord client
601        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}