twitch_oauth2/scopes/
validator.rs

1//! Validator used for checking scopes in a token.
2use std::borrow::Cow;
3
4use super::Scope;
5
6/// A collection of validators
7pub type Validators = Cow<'static, [Validator]>;
8
9/// A [validator](Validator) is a way to check if an array of scopes matches a predicate.
10///
11/// Can be constructed easily with the [validator!](crate::validator) macro.
12///
13/// # Examples
14///
15/// ```rust, no_run
16/// use twitch_oauth2::{validator, Scope, TwitchToken as _, UserToken};
17///
18/// let token: UserToken = token();
19/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
20/// assert!(validator.matches(token.scopes()));
21///
22/// # pub fn token() -> UserToken { todo!() }
23/// ```
24#[derive(Clone, PartialEq)]
25#[non_exhaustive]
26pub enum Validator {
27    /// A scope
28    Scope(Scope),
29    /// Matches true if all validators passed inside return true
30    All(Sized<Validators>),
31    /// Matches true if **any** validator passed inside returns true
32    Any(Sized<Validators>),
33    /// Matches true if all validators passed inside matches false
34    Not(Sized<Validators>),
35}
36
37impl std::fmt::Debug for Validator {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Validator::Scope(scope) => scope.fmt(f),
41            Validator::All(Sized(all)) => f.debug_tuple("All").field(all).finish(),
42            Validator::Any(Sized(any)) => f.debug_tuple("Any").field(any).finish(),
43            Validator::Not(Sized(not)) => f.debug_tuple("Not").field(not).finish(),
44        }
45    }
46}
47
48impl std::fmt::Display for Validator {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        // dont allocate if we can avoid it, instead we map over the validators, and use write!
51        match self {
52            Validator::Scope(scope) => scope.fmt(f),
53            Validator::All(Sized(all)) => {
54                write!(f, "(")?;
55                for (i, v) in all.iter().enumerate() {
56                    if i != 0 {
57                        write!(f, " and ")?;
58                    }
59                    write!(f, "{}", v)?;
60                }
61                write!(f, ")")
62            }
63            Validator::Any(Sized(any)) => {
64                write!(f, "(")?;
65                for (i, v) in any.iter().enumerate() {
66                    if i != 0 {
67                        write!(f, " or ")?;
68                    }
69                    write!(f, "{}", v)?;
70                }
71                write!(f, ")")
72            }
73            Validator::Not(Sized(not)) => {
74                write!(f, "not(")?;
75                for (i, v) in not.iter().enumerate() {
76                    if i != 0 {
77                        write!(f, ", ")?;
78                    }
79                    write!(f, "{}", v)?;
80                }
81                write!(f, ")")
82            }
83        }
84    }
85}
86
87impl Validator {
88    /// Checks if the given scopes match the predicate.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use twitch_oauth2::{validator, Scope};
94    ///
95    /// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
96    /// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
97    /// assert!(validator.matches(scopes));
98    /// assert!(!validator.matches(&scopes[..1]));
99    /// ```
100    #[must_use]
101    pub fn matches(&self, scopes: &[Scope]) -> bool {
102        match &self {
103            Validator::Scope(scope) => scopes.contains(scope),
104            Validator::All(Sized(validators)) => validators.iter().all(|v| v.matches(scopes)),
105            Validator::Any(Sized(validators)) => validators.iter().any(|v| v.matches(scopes)),
106            Validator::Not(Sized(validator)) => !validator.iter().any(|v| v.matches(scopes)),
107        }
108    }
109
110    /// Returns a validator only containing the unmatched scopes.
111    ///
112    /// # Examples
113    ///
114    /// ```rust
115    /// use twitch_oauth2::{validator, Scope};
116    ///
117    /// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
118    ///
119    /// let scopes = &[Scope::ChatEdit, Scope::ChatRead];
120    /// assert_eq!(validator.missing(scopes), None);
121    /// ```
122    ///
123    /// ```rust
124    /// use twitch_oauth2::{validator, Scope};
125    ///
126    /// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
127    ///
128    /// let scopes = &[Scope::ChatEdit];
129    /// if let Some(v) = validator.missing(scopes) {
130    ///     println!("Missing scopes: {}", v);
131    /// }
132    /// ```
133    ///
134    /// ```rust
135    /// use twitch_oauth2::{validator, Scope};
136    ///
137    /// let validator = validator!(
138    ///     any(
139    ///         Scope::ModeratorReadBlockedTerms,
140    ///         Scope::ModeratorManageBlockedTerms
141    ///     ),
142    ///     any(
143    ///         Scope::ModeratorReadChatSettings,
144    ///         Scope::ModeratorManageChatSettings
145    ///     )
146    /// );
147    ///
148    /// let scopes = &[Scope::ModeratorReadBlockedTerms];
149    /// let missing = validator.missing(scopes).unwrap();
150    /// // We're missing either of the chat settings scopes
151    /// assert!(missing.matches(&[Scope::ModeratorReadChatSettings]));
152    /// assert!(missing.matches(&[Scope::ModeratorManageChatSettings]));
153    /// ```
154    pub fn missing(&self, scopes: &[Scope]) -> Option<Validator> {
155        if self.matches(scopes) {
156            return None;
157        }
158        // a recursive prune approach, if a validator matches, we prune it.
159        // TODO: There's a bit of allocation going on here, maybe we can remove it with some kind of descent
160        match &self {
161            Validator::Scope(scope) => {
162                if scopes.contains(scope) {
163                    None
164                } else {
165                    Some(Validator::Scope(scope.clone()))
166                }
167            }
168            Validator::All(Sized(validators)) => {
169                let mut missing = validators
170                    .iter()
171                    .filter_map(|v| v.missing(scopes))
172                    .collect::<Vec<_>>();
173
174                if missing.is_empty() {
175                    None
176                } else if missing.len() == 1 {
177                    Some(missing.remove(0))
178                } else {
179                    Some(Validator::All(Sized(Cow::Owned(missing))))
180                }
181            }
182            Validator::Any(Sized(validators)) => {
183                let mut missing = validators
184                    .iter()
185                    .filter(|v| !v.matches(scopes))
186                    .filter_map(|v| v.missing(scopes))
187                    .collect::<Vec<_>>();
188
189                if missing.is_empty() {
190                    None
191                } else if missing.len() == 1 {
192                    Some(missing.remove(0))
193                } else {
194                    Some(Validator::Any(Sized(Cow::Owned(missing))))
195                }
196            }
197            Validator::Not(Sized(validators)) => {
198                // not is special, it's a negation, so a match is a failure.
199                // we find out if the validators inside matches (e.g the scopes exists),
200                // if they exist they are bad.
201                // a validator should preferably not use not, because scopes are additive.
202
203                let matching = validators
204                    .iter()
205                    .filter(|v| v.matches(scopes))
206                    .collect::<Vec<_>>();
207
208                if matching.is_empty() {
209                    None
210                } else {
211                    Some(Validator::Not(Sized(Cow::Owned(
212                        matching.into_iter().cloned().collect(),
213                    ))))
214                }
215            }
216        }
217    }
218
219    /// Create a [Validator] which matches if the scope is present.
220    pub const fn scope(scope: Scope) -> Self { Validator::Scope(scope) }
221
222    /// Create a [Validator] which matches if all validators passed inside matches true.
223    pub const fn all_multiple(ands: &'static [Validator]) -> Self {
224        Validator::All(Sized(Cow::Borrowed(ands)))
225    }
226
227    /// Create a [Validator] which matches if **any** validator passed inside matches true.
228    pub const fn any_multiple(anys: &'static [Validator]) -> Self {
229        Validator::Any(Sized(Cow::Borrowed(anys)))
230    }
231
232    /// Create a [Validator] which matches if all validators passed inside matches false.
233    pub const fn not(not: &'static Validator) -> Self {
234        Validator::Not(Sized(Cow::Borrowed(std::slice::from_ref(not))))
235    }
236
237    /// Convert [Self] to [Self]
238    ///
239    /// # Notes
240    ///
241    /// This function doesn't do anything, but it powers the [validator!][crate::validator] macro
242    #[doc(hidden)]
243    pub const fn to_validator(self) -> Self { self }
244}
245
246// https://github.com/rust-lang/rust/issues/47032#issuecomment-568784919
247/// Hack for making `T: Sized`
248#[derive(Debug, Clone, PartialEq)]
249#[repr(transparent)]
250pub struct Sized<T>(pub T);
251
252impl From<Scope> for Validator {
253    fn from(scope: Scope) -> Self { Validator::scope(scope) }
254}
255
256/// A [validator](Validator) is a way to check if a slice of scopes matches a predicate.
257///
258/// Uses a functional style to compose the predicate. Can be used in const context.
259///
260/// # Supported operators
261///
262/// * `not(...)`
263///   * negates the validator passed inside, can only take one argument
264/// * `all(...)`
265///   * returns true if all validators passed inside return true
266/// * `any(...)`
267///   * returns true if **any** validator passed inside returns true
268///
269/// # Examples
270///
271/// ```rust, no_run
272/// use twitch_oauth2::{validator, Scope, TwitchToken as _, UserToken};
273///
274/// let token: UserToken = token();
275/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
276/// assert!(validator.matches(token.scopes()));
277///
278/// # pub fn token() -> UserToken { todo!() }
279/// ```
280///
281/// ## Multiple scopes
282///
283/// ```rust
284/// use twitch_oauth2::{validator, Scope};
285///
286/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
287/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
288/// assert!(validator.matches(scopes));
289/// assert!(!validator.matches(&scopes[..1]));
290/// ```
291///
292/// ## Multiple scopes with explicit all(...)
293///
294/// ```rust
295/// use twitch_oauth2::{validator, Scope};
296///
297/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
298/// let validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
299/// assert!(validator.matches(scopes));
300/// assert!(!validator.matches(&scopes[..1]));
301/// ```
302///
303/// ## Multiple scopes with nested any(...)
304///
305/// ```rust
306/// use twitch_oauth2::{validator, Scope};
307///
308/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
309/// let validator = validator!(
310///     Scope::ChatEdit,
311///     any(Scope::ChatRead, Scope::ChannelReadSubscriptions)
312/// );
313/// assert!(validator.matches(scopes));
314/// assert!(!validator.matches(&scopes[1..]));
315/// ```
316///
317/// ## Not
318///
319/// ```rust
320/// use twitch_oauth2::{validator, Scope};
321///
322/// let scopes: &[Scope] = &[Scope::ChatRead];
323/// let validator = validator!(not(Scope::ChatEdit));
324/// assert!(validator.matches(scopes));
325/// ```
326///
327/// ## Combining other validators
328///
329/// ```
330/// use twitch_oauth2::{validator, Scope, Validator};
331///
332/// let scopes: &[Scope] = &[
333///     Scope::ChatEdit,
334///     Scope::ChatRead,
335///     Scope::ModeratorManageAutoMod,
336///     Scope::ModerationRead,
337/// ];
338/// const CHAT_SCOPES: Validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
339/// const MODERATOR_SCOPES: Validator =
340///     validator!(Scope::ModerationRead, Scope::ModeratorManageAutoMod);
341/// const COMBINED: Validator = validator!(CHAT_SCOPES, MODERATOR_SCOPES);
342/// assert!(COMBINED.matches(scopes));
343/// assert!(!COMBINED.matches(&scopes[1..]));
344/// ```
345///
346/// ## Empty
347///
348/// ```rust
349/// use twitch_oauth2::{validator, Scope};
350///
351/// let scopes: &[Scope] = &[Scope::ChatRead];
352/// let validator = validator!();
353/// assert!(validator.matches(scopes));
354/// ```
355///
356/// ## Invalid examples
357///
358/// ### Invalid usage of not(...)
359///
360/// ```compile_fail
361/// use twitch_oauth2::{Scope, validator};
362///
363/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
364/// let validator = validator!(not(Scope::ChatEdit, Scope::ChatRead));
365/// assert!(validator.matches(scopes));
366/// ```
367///
368/// ### Invalid operator
369///
370/// ```compile_fail
371/// use twitch_oauth2::{Scope, validator};
372///
373/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
374/// let validator = validator!(xor(Scope::ChatEdit, Scope::ChatRead));
375/// assert!(validator.matches(scopes));
376/// ```
377#[macro_export]
378macro_rules! validator {
379    ($operator:ident($($scopes:tt)+)) => {{
380        $crate::validator_logic!(@$operator $($scopes)*)
381    }};
382    ($scope:expr $(,)?) => {{
383        $scope.to_validator()
384    }};
385    ($($all:tt)+) => {{
386        $crate::validator_logic!(@all $($all)*)
387    }};
388    () => {{
389        $crate::Validator::all_multiple(&[])
390    }};
391}
392
393/// Logical operators for the [validator!][crate::validator] macro.
394#[doc(hidden)]
395#[macro_export]
396macro_rules! validator_logic {
397    (@all $($scope:tt)+) => {{
398        const MULT: &[$crate::Validator] = &$crate::validator_accumulate![@down [] $($scope)*];
399        $crate::Validator::all_multiple(MULT)
400    }};
401    (@any $($scope:tt)+) => {{
402        const MULT: &[$crate::Validator] = &$crate::validator_accumulate![@down [] $($scope)*];
403        $crate::Validator::any_multiple(MULT)
404    }};
405    (@not $($scope:tt)+) => {{
406        $crate::validator_logic!(@notend $($scope)*);
407        const NOT: &[$crate::Validator] = &[$crate::validator!($($scope)*)];
408        $crate::Validator::not(&NOT[0])
409    }};
410    (@notend $e:expr) => {};
411    (@notend $e:expr, $($t:tt)*) => {compile_error!("not(...) takes only one argument")};
412    (@$operator:ident $($rest:tt)*) => {
413        compile_error!(concat!("unknown operator `", stringify!($operator), "`, only `all`, `any` and `not` are supported"))
414    }
415}
416
417/// Accumulator for the [validator!][crate::validator] macro.
418// Thanks to danielhenrymantilla, the macro wizard
419#[doc(hidden)]
420#[macro_export]
421macro_rules! validator_accumulate {
422    // inner operator
423    (@down
424        [$($acc:tt)*]
425        $operator:ident($($all:tt)*) $(, $($rest:tt)* )?
426    ) => (
427        $crate::validator_accumulate![@down
428            [$($acc)* $crate::validator!($operator($($all)*)),]
429            $($($rest)*)?
430        ]
431    );
432    // inner scope
433    (@down
434        [$($acc:tt)*]
435        $scope:expr $(, $($rest:tt)* )?
436    ) => (
437        $crate::validator_accumulate![@down
438            [$($acc)* $crate::validator!($scope),]
439            $($($rest)*)?
440        ]
441    );
442    // nothing left
443    (@down
444        [$($output:tt)*] $(,)?
445    ) => (
446        [ $($output)* ]
447    );
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::Scope;
454
455    #[test]
456    fn valid_basic() {
457        let scopes = &[Scope::ChatEdit, Scope::ChatRead];
458        const VALIDATOR: Validator = validator!(Scope::ChatEdit, Scope::ChatRead);
459        dbg!(&VALIDATOR);
460        assert!(VALIDATOR.matches(scopes));
461    }
462
463    #[test]
464    fn valid_all() {
465        let scopes = &[Scope::ChatEdit, Scope::ChatRead];
466        const VALIDATOR: Validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
467        dbg!(&VALIDATOR);
468        assert!(VALIDATOR.matches(scopes));
469    }
470
471    #[test]
472    fn valid_any() {
473        let scopes = &[Scope::ChatEdit, Scope::ModerationRead];
474        const VALIDATOR: Validator =
475            validator!(Scope::ChatEdit, any(Scope::ChatRead, Scope::ModerationRead));
476        dbg!(&VALIDATOR);
477        assert!(VALIDATOR.matches(scopes));
478    }
479
480    #[test]
481    fn valid_not() {
482        let scopes = &[Scope::ChannelEditCommercial, Scope::ChatRead];
483        const VALIDATOR: Validator = validator!(not(Scope::ChatEdit));
484        dbg!(&VALIDATOR);
485        assert!(VALIDATOR.matches(scopes));
486    }
487
488    #[test]
489    fn valid_strange() {
490        let scopes = &[Scope::ChatEdit, Scope::ModerationRead, Scope::UserEdit];
491        let scopes_1 = &[Scope::ChatEdit, Scope::ChatRead];
492        const VALIDATOR: Validator = validator!(
493            Scope::ChatEdit,
494            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
495        );
496        dbg!(&VALIDATOR);
497        assert!(VALIDATOR.matches(scopes));
498        assert!(VALIDATOR.matches(scopes_1));
499    }
500    #[test]
501    fn valid_strange_not() {
502        let scopes = &[Scope::ModerationRead, Scope::UserEdit];
503        let scopes_1 = &[Scope::ChatEdit, Scope::ChatRead];
504        const VALIDATOR: Validator = validator!(
505            not(Scope::ChatEdit),
506            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
507        );
508        dbg!(&VALIDATOR);
509        assert!(VALIDATOR.matches(scopes));
510        assert!(!VALIDATOR.matches(scopes_1));
511    }
512
513    #[test]
514    fn missing() {
515        let scopes = &[Scope::ChatEdit, Scope::ModerationRead];
516        const VALIDATOR: Validator = validator!(
517            Scope::ChatEdit,
518            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
519        );
520        dbg!(&VALIDATOR);
521        let missing = VALIDATOR.missing(scopes).unwrap();
522        dbg!(&missing);
523        assert_eq!(format!("{}", missing), "(chat:read or user:edit)");
524
525        const NOT_VALIDATOR: Validator = validator!(all(
526            not(all(Scope::ChatEdit, Scope::ModerationRead)), // we don't want both of these
527            Scope::ChatRead,
528            Scope::UserEdit,
529            any(Scope::ModerationRead, not(Scope::UserEdit)) // we don't want user:edit or we want moderation:read
530        ));
531        let missing = NOT_VALIDATOR.missing(scopes).unwrap();
532        dbg!(&missing);
533        assert_eq!(
534            format!("{}", missing),
535            "(not((chat:edit and moderation:read)) and chat:read and user:edit)"
536        );
537    }
538
539    #[test]
540    fn display() {
541        const COMPLEX_VALIDATOR: Validator = validator!(
542            Scope::ChatEdit,
543            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
544        );
545        assert_eq!(
546            format!("{}", COMPLEX_VALIDATOR),
547            "(chat:edit and (chat:read or (moderation:read and user:edit)))"
548        );
549    }
550}