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, AppAccessToken, Scope, TwitchToken as _};
17///
18/// let token: AppAccessToken = token();
19/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
20/// assert!(validator.matches(token.scopes()));
21///
22/// # pub fn token() -> AppAccessToken { todo!() }
23/// ```
24#[derive(Clone)]
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 Validator {
49    /// Checks if the given scopes match the predicate.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use twitch_oauth2::{validator, Scope};
55    ///
56    /// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
57    /// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
58    /// assert!(validator.matches(scopes));
59    /// assert!(!validator.matches(&scopes[..1]));
60    /// ```
61    #[must_use]
62    pub fn matches(&self, scopes: &[Scope]) -> bool {
63        match &self {
64            Validator::Scope(scope) => scopes.contains(scope),
65            Validator::All(Sized(validators)) => validators.iter().all(|v| v.matches(scopes)),
66            Validator::Any(Sized(validators)) => validators.iter().any(|v| v.matches(scopes)),
67            Validator::Not(Sized(validator)) => !validator.iter().any(|v| v.matches(scopes)),
68        }
69    }
70
71    /// Create a [Validator] which matches if the scope is present.
72    pub const fn scope(scope: Scope) -> Self { Validator::Scope(scope) }
73
74    /// Create a [Validator] which matches if all validators passed inside matches true.
75    pub const fn all_multiple(ands: &'static [Validator]) -> Self {
76        Validator::All(Sized(Cow::Borrowed(ands)))
77    }
78
79    /// Create a [Validator] which matches if **any** validator passed inside matches true.
80    pub const fn any_multiple(anys: &'static [Validator]) -> Self {
81        Validator::Any(Sized(Cow::Borrowed(anys)))
82    }
83
84    /// Create a [Validator] which matches if all validators passed inside matches false.
85    pub const fn not(not: &'static Validator) -> Self {
86        Validator::Not(Sized(Cow::Borrowed(std::slice::from_ref(not))))
87    }
88
89    /// Convert [Self] to [Self]
90    ///
91    /// # Notes
92    ///
93    /// This function doesn't do anything, but it powers the [validator!] macro
94    #[doc(hidden)]
95    pub const fn to_validator(self) -> Self { self }
96}
97
98// https://github.com/rust-lang/rust/issues/47032#issuecomment-568784919
99/// Hack for making `T: Sized`
100#[derive(Debug, Clone)]
101#[repr(transparent)]
102pub struct Sized<T>(pub T);
103
104impl From<Scope> for Validator {
105    fn from(scope: Scope) -> Self { Validator::scope(scope) }
106}
107
108/// A [validator](Validator) is a way to check if a slice of scopes matches a predicate.
109///
110/// Uses a functional style to compose the predicate. Can be used in const context.
111///
112/// # Supported operators
113///
114/// * `not(...)`
115///   * negates the validator passed inside, can only take one argument
116/// * `all(...)`
117///   * returns true if all validators passed inside return true
118/// * `any(...)`
119///   * returns true if **any** validator passed inside returns true
120///
121/// # Examples
122///
123/// ```rust, no_run
124/// use twitch_oauth2::{validator, AppAccessToken, Scope, TwitchToken as _};
125///
126/// let token: AppAccessToken = token();
127/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
128/// assert!(validator.matches(token.scopes()));
129///
130/// # pub fn token() -> AppAccessToken { todo!() }
131/// ```
132///
133/// ## Multiple scopes
134///
135/// ```rust
136/// use twitch_oauth2::{validator, Scope};
137///
138/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
139/// let validator = validator!(Scope::ChatEdit, Scope::ChatRead);
140/// assert!(validator.matches(scopes));
141/// assert!(!validator.matches(&scopes[..1]));
142/// ```
143///
144/// ## Multiple scopes with explicit all(...)
145///
146/// ```rust
147/// use twitch_oauth2::{validator, Scope};
148///
149/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
150/// let validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
151/// assert!(validator.matches(scopes));
152/// assert!(!validator.matches(&scopes[..1]));
153/// ```
154///
155/// ## Multiple scopes with nested any(...)
156///
157/// ```rust
158/// use twitch_oauth2::{validator, Scope};
159///
160/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
161/// let validator = validator!(
162///     Scope::ChatEdit,
163///     any(Scope::ChatRead, Scope::ChannelReadSubscriptions)
164/// );
165/// assert!(validator.matches(scopes));
166/// assert!(!validator.matches(&scopes[1..]));
167/// ```
168///
169/// ## Not
170///
171/// ```rust
172/// use twitch_oauth2::{validator, Scope};
173///
174/// let scopes: &[Scope] = &[Scope::ChatRead];
175/// let validator = validator!(not(Scope::ChatEdit));
176/// assert!(validator.matches(scopes));
177/// ```
178///
179/// ## Combining other validators
180///
181/// ```
182/// use twitch_oauth2::{validator, Scope, Validator};
183///
184/// let scopes: &[Scope] = &[
185///     Scope::ChatEdit,
186///     Scope::ChatRead,
187///     Scope::ModeratorManageAutoMod,
188///     Scope::ModerationRead,
189/// ];
190/// const CHAT_SCOPES: Validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
191/// const MODERATOR_SCOPES: Validator =
192///     validator!(Scope::ModerationRead, Scope::ModeratorManageAutoMod);
193/// const COMBINED: Validator = validator!(CHAT_SCOPES, MODERATOR_SCOPES);
194/// assert!(COMBINED.matches(scopes));
195/// assert!(!COMBINED.matches(&scopes[1..]));
196/// ```
197///
198/// ## Empty
199///
200/// ```rust
201/// use twitch_oauth2::{validator, Scope};
202///
203/// let scopes: &[Scope] = &[Scope::ChatRead];
204/// let validator = validator!();
205/// assert!(validator.matches(scopes));
206/// ```
207///
208/// ## Invalid examples
209///
210/// ### Invalid usage of not(...)
211///
212/// ```compile_fail
213/// use twitch_oauth2::{Scope, validator};
214///
215/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
216/// let validator = validator!(not(Scope::ChatEdit, Scope::ChatRead));
217/// assert!(validator.matches(scopes));
218/// ```
219///
220/// ### Invalid operator
221///
222/// ```compile_fail
223/// use twitch_oauth2::{Scope, validator};
224///
225/// let scopes: &[Scope] = &[Scope::ChatEdit, Scope::ChatRead];
226/// let validator = validator!(xor(Scope::ChatEdit, Scope::ChatRead));
227/// assert!(validator.matches(scopes));
228/// ```
229#[macro_export]
230macro_rules! validator {
231    ($operator:ident($($scopes:tt)+)) => {{
232        $crate::validator_logic!(@$operator $($scopes)*)
233    }};
234    ($scope:expr $(,)?) => {{
235        $scope.to_validator()
236    }};
237    ($($all:tt)+) => {{
238        $crate::validator_logic!(@all $($all)*)
239    }};
240    () => {{
241        $crate::Validator::all_multiple(&[])
242    }};
243}
244
245/// Logical operators for the [validator!] macro.
246#[doc(hidden)]
247#[macro_export]
248macro_rules! validator_logic {
249    (@all $($scope:tt)+) => {{
250        const MULT: &[$crate::Validator] = &$crate::validator_accumulate![@down [] $($scope)*];
251        $crate::Validator::all_multiple(MULT)
252    }};
253    (@any $($scope:tt)+) => {{
254        const MULT: &[$crate::Validator] = &$crate::validator_accumulate![@down [] $($scope)*];
255        $crate::Validator::any_multiple(MULT)
256    }};
257    (@not $($scope:tt)+) => {{
258        $crate::validator_logic!(@notend $($scope)*);
259        const NOT: &[$crate::Validator] = &[$crate::validator!($($scope)*)];
260        $crate::Validator::not(&NOT[0])
261    }};
262    (@notend $e:expr) => {};
263    (@notend $e:expr, $($t:tt)*) => {compile_error!("not(...) takes only one argument")};
264    (@$operator:ident $($rest:tt)*) => {
265        compile_error!(concat!("unknown operator `", stringify!($operator), "`, only `all`, `any` and `not` are supported"))
266    }
267}
268
269/// Accumulator for the [validator!] macro.
270// Thanks to danielhenrymantilla, the macro wizard
271#[doc(hidden)]
272#[macro_export]
273macro_rules! validator_accumulate {
274    // inner operator
275    (@down
276        [$($acc:tt)*]
277        $operator:ident($($all:tt)*) $(, $($rest:tt)* )?
278    ) => (
279        $crate::validator_accumulate![@down
280            [$($acc)* $crate::validator!($operator($($all)*)),]
281            $($($rest)*)?
282        ]
283    );
284    // inner scope
285    (@down
286        [$($acc:tt)*]
287        $scope:expr $(, $($rest:tt)* )?
288    ) => (
289        $crate::validator_accumulate![@down
290            [$($acc)* $crate::validator!($scope),]
291            $($($rest)*)?
292        ]
293    );
294    // nothing left
295    (@down
296        [$($output:tt)*] $(,)?
297    ) => (
298        [ $($output)* ]
299    );
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::Scope;
306
307    #[test]
308    fn valid_basic() {
309        let scopes = &[Scope::ChatEdit, Scope::ChatRead];
310        const VALIDATOR: Validator = validator!(Scope::ChatEdit, Scope::ChatRead);
311        dbg!(&VALIDATOR);
312        assert!(VALIDATOR.matches(scopes));
313    }
314
315    #[test]
316    fn valid_all() {
317        let scopes = &[Scope::ChatEdit, Scope::ChatRead];
318        const VALIDATOR: Validator = validator!(all(Scope::ChatEdit, Scope::ChatRead));
319        dbg!(&VALIDATOR);
320        assert!(VALIDATOR.matches(scopes));
321    }
322
323    #[test]
324    fn valid_any() {
325        let scopes = &[Scope::ChatEdit, Scope::ModerationRead];
326        const VALIDATOR: Validator =
327            validator!(Scope::ChatEdit, any(Scope::ChatRead, Scope::ModerationRead));
328        dbg!(&VALIDATOR);
329        assert!(VALIDATOR.matches(scopes));
330    }
331
332    #[test]
333    fn valid_not() {
334        let scopes = &[Scope::ChannelEditCommercial, Scope::ChatRead];
335        const VALIDATOR: Validator = validator!(not(Scope::ChatEdit));
336        dbg!(&VALIDATOR);
337        assert!(VALIDATOR.matches(scopes));
338    }
339
340    #[test]
341    fn valid_strange() {
342        let scopes = &[Scope::ChatEdit, Scope::ModerationRead, Scope::UserEdit];
343        let scopes_1 = &[Scope::ChatEdit, Scope::ChatRead];
344        const VALIDATOR: Validator = validator!(
345            Scope::ChatEdit,
346            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
347        );
348        dbg!(&VALIDATOR);
349        assert!(VALIDATOR.matches(scopes));
350        assert!(VALIDATOR.matches(scopes_1));
351    }
352    #[test]
353    fn valid_strange_not() {
354        let scopes = &[Scope::ModerationRead, Scope::UserEdit];
355        let scopes_1 = &[Scope::ChatEdit, Scope::ChatRead];
356        const VALIDATOR: Validator = validator!(
357            not(Scope::ChatEdit),
358            any(Scope::ChatRead, all(Scope::ModerationRead, Scope::UserEdit))
359        );
360        dbg!(&VALIDATOR);
361        assert!(VALIDATOR.matches(scopes));
362        assert!(!VALIDATOR.matches(scopes_1));
363    }
364}