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}