twilight_mention/parse/
iter.rs

1use super::ParseMention;
2use std::{marker::PhantomData, str::CharIndices};
3
4/// Iterator of mentions within a buffer.
5///
6/// Unlike when parsing a mention directly via [`ParseMention::parse`],
7/// incomplete mentions - such as when a "<" is found but not valid trailing
8/// mention syntax - will not result in errors.
9///
10/// The iterator returns items consisting of 3 items: the mention itself
11/// followed by the starting index and ending index of the mention's source in
12/// the buffer.
13///
14/// # Examples
15///
16/// Iterate over all of the mentioned users:
17///
18/// ```
19/// use twilight_mention::ParseMention;
20/// use twilight_model::id::{marker::UserMarker, Id};
21///
22/// let buf = "<@123> some <@456> users <@789>!";
23/// let mut iter = Id::<UserMarker>::iter(buf);
24/// assert!(matches!(iter.next(), Some((user, _, _)) if user.get() == 123));
25/// assert!(matches!(iter.next(), Some((user, _, _)) if user.get() == 456));
26/// assert!(matches!(iter.next(), Some((user, _, _)) if user.get() == 789));
27/// ```
28#[derive(Clone, Debug)]
29pub struct MentionIter<'a, T> {
30    buf: &'a str,
31    chars: CharIndices<'a>,
32    phantom: PhantomData<T>,
33}
34
35impl<'a, T> MentionIter<'a, T> {
36    #[must_use]
37    pub(in crate::parse) fn new(buf: &'a str) -> Self {
38        let chars = buf.char_indices();
39
40        Self {
41            buf,
42            chars,
43            phantom: PhantomData,
44        }
45    }
46
47    /// Return an immutable reference to the underlying buffer of the iterator.
48    #[must_use]
49    pub const fn as_str(&self) -> &'a str {
50        self.buf
51    }
52}
53
54impl<T: ParseMention> Iterator for MentionIter<'_, T> {
55    /// Found mention followed by the start and ending indexes in the source
56    /// string returned by [`as_str`].
57    ///
58    /// [`as_str`]: Self::as_str
59    type Item = (T, usize, usize);
60
61    fn next(&mut self) -> Option<Self::Item> {
62        // We want to take care of our input and make sure we're working with
63        // chars here and not just individual bytes. We also want to not use
64        // consuming methods of the iterator, so this will get a little weird.
65        loop {
66            let (start, '<') = self.chars.next()? else {
67                continue;
68            };
69
70            let mut found = false;
71
72            for sigil in T::SIGILS {
73                if self.chars.as_str().starts_with(sigil) {
74                    found = true;
75
76                    for _ in 0..sigil.chars().count() {
77                        self.chars.next();
78                    }
79                }
80            }
81
82            if !found {
83                continue;
84            }
85
86            let Some((end, _)) = self.chars.find(|c| c.1 == '>') else {
87                continue;
88            };
89
90            let buf = self.buf.get(start..=end)?;
91
92            if let Ok(id) = T::parse(buf) {
93                return Some((id, start, end));
94            }
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use crate::timestamp::{Timestamp, TimestampStyle};
102
103    use super::{
104        super::{MentionType, ParseMention},
105        MentionIter,
106    };
107    use static_assertions::{assert_impl_all, assert_obj_safe};
108    use std::fmt::Debug;
109    use twilight_model::id::{
110        marker::{ChannelMarker, EmojiMarker, RoleMarker, UserMarker},
111        Id,
112    };
113
114    assert_impl_all!(MentionIter<'_, Id<ChannelMarker>>: Clone, Debug, Iterator, Send, Sync);
115    assert_impl_all!(MentionIter<'_, Id<EmojiMarker>>: Clone, Debug, Iterator, Send, Sync);
116    assert_impl_all!(MentionIter<'_, MentionType>: Clone, Debug, Iterator, Send, Sync);
117    assert_impl_all!(MentionIter<'_, Id<RoleMarker>>: Clone, Debug, Iterator, Send, Sync);
118    assert_impl_all!(MentionIter<'_, Id<UserMarker>>: Clone, Debug, Iterator, Send, Sync);
119    assert_obj_safe!(
120        MentionIter<'_, Id<ChannelMarker>>,
121        MentionIter<'_, Id<EmojiMarker>>,
122        MentionIter<'_, MentionType>,
123        MentionIter<'_, Id<RoleMarker>>,
124        MentionIter<'_, Id<UserMarker>>,
125    );
126
127    #[test]
128    fn iter_channel_id() {
129        let mut iter = Id::<ChannelMarker>::iter("<#123>");
130        assert_eq!(Id::new(123), iter.next().unwrap().0);
131        assert!(iter.next().is_none());
132    }
133
134    #[test]
135    fn iter_multiple_ids() {
136        let buf = "one <@123>two<#456><@789> ----";
137        let mut iter = Id::<UserMarker>::iter(buf);
138        assert_eq!(Id::new(123), iter.next().unwrap().0);
139        let (mention, start, end) = iter.next().unwrap();
140        assert_eq!(Id::new(789), mention);
141        assert_eq!(19, start);
142        assert_eq!(24, end);
143        assert!(iter.next().is_none());
144    }
145
146    #[test]
147    fn iter_emoji_ids() {
148        let mut iter = Id::<EmojiMarker>::iter("some <:name:123> emojis <:emoji:456>");
149        assert_eq!(Id::new(123), iter.next().unwrap().0);
150        assert_eq!(Id::new(456), iter.next().unwrap().0);
151        assert!(iter.next().is_none());
152    }
153
154    #[test]
155    fn iter_mention_type() {
156        let mut iter = MentionType::iter("<#12><:name:34><@&56><@78>");
157        assert_eq!(MentionType::Channel(Id::new(12)), iter.next().unwrap().0);
158        assert_eq!(MentionType::Emoji(Id::new(34)), iter.next().unwrap().0);
159        assert_eq!(MentionType::Role(Id::new(56)), iter.next().unwrap().0);
160        assert_eq!(MentionType::User(Id::new(78)), iter.next().unwrap().0);
161        assert!(iter.next().is_none());
162    }
163
164    #[test]
165    fn iter_mention_type_with_timestamp() {
166        let mut iter = MentionType::iter("<#12> <t:34> <t:56:d>");
167        assert_eq!(MentionType::Channel(Id::new(12)), iter.next().unwrap().0);
168        assert_eq!(
169            MentionType::Timestamp(Timestamp::new(34, None)),
170            iter.next().unwrap().0
171        );
172        assert_eq!(
173            MentionType::Timestamp(Timestamp::new(56, Some(TimestampStyle::ShortDate))),
174            iter.next().unwrap().0
175        );
176        assert!(iter.next().is_none());
177    }
178
179    #[test]
180    fn iter_role_ids() {
181        let mut iter = Id::<RoleMarker>::iter("some <@&123> roles <@&456>");
182        assert_eq!(Id::new(123), iter.next().unwrap().0);
183        assert_eq!(Id::new(456), iter.next().unwrap().0);
184        assert!(iter.next().is_none());
185    }
186
187    #[test]
188    fn iter_timestamps() {
189        let mut iter = Timestamp::iter("some <t:123> roles <t:456:t>");
190        assert_eq!(Timestamp::new(123, None), iter.next().unwrap().0);
191        assert_eq!(
192            Timestamp::new(456, Some(TimestampStyle::ShortTime)),
193            iter.next().unwrap().0
194        );
195        assert!(iter.next().is_none());
196    }
197
198    #[test]
199    fn iter_user_ids() {
200        let mut iter = Id::<UserMarker>::iter("some <@123>users<@456>");
201        assert_eq!(Id::new(123), iter.next().unwrap().0);
202        assert_eq!(Id::new(456), iter.next().unwrap().0);
203        assert!(iter.next().is_none());
204    }
205
206    #[test]
207    fn iter_no_id() {
208        let mention = "this is not <# actually a mention";
209        let mut iter = Id::<ChannelMarker>::iter(mention);
210
211        assert!(iter.next().is_none());
212    }
213
214    #[test]
215    fn iter_ignores_other_types() {
216        let mention = "<#123> <:name:456> <@&789>";
217        let mut iter = Id::<UserMarker>::iter(mention);
218
219        assert!(iter.next().is_none());
220    }
221
222    #[test]
223    fn iter_as_str() {
224        let buf = "a buf";
225        let mut iter = Id::<RoleMarker>::iter(buf);
226        assert_eq!(buf, iter.as_str());
227        // Advancing still returns the complete buffer.
228        assert!(iter.next().is_none());
229        assert_eq!(buf, iter.as_str());
230    }
231}