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}