1pub fn parse_scopes(input: &str) -> Vec<String> {
5 let mut scopes: Vec<String> = input
6 .split(|c: char| c.is_whitespace() || c == ',')
7 .filter(|s| !s.is_empty())
8 .map(|s| s.trim().to_lowercase())
9 .collect();
10 scopes.sort();
11 scopes.dedup();
12 scopes
13}
14
15pub fn validate_scopes(requested: &[String], allowed: &[&str]) -> Result<Vec<String>, ScopeError> {
19 for scope in requested {
20 if !allowed.contains(&scope.as_str()) {
21 return Err(ScopeError::UnknownScope(scope.clone()));
22 }
23 }
24 Ok(requested.to_vec())
25}
26
27pub fn scopes_subset(requested: &[String], allowed: &[String]) -> bool {
29 requested.iter().all(|r| allowed.contains(r))
30}
31
32#[derive(Debug, thiserror::Error)]
34pub enum ScopeError {
35 #[error("unknown scope: {0}")]
37 UnknownScope(String),
38}
39
40#[cfg(test)]
41#[allow(clippy::unwrap_used, clippy::expect_used)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn parse_scopes_whitespace() {
48 assert_eq!(
49 parse_scopes("read write admin"),
50 vec!["admin", "read", "write"]
51 );
52 }
53
54 #[test]
55 fn parse_scopes_comma() {
56 assert_eq!(
57 parse_scopes("read,write,admin"),
58 vec!["admin", "read", "write"]
59 );
60 }
61
62 #[test]
63 fn parse_scopes_dedup() {
64 assert_eq!(parse_scopes("read read"), vec!["read"]);
65 }
66
67 #[test]
68 fn validate_scopes_ok() {
69 let requested = vec!["read".into(), "write".into()];
70 let allowed = &["read", "write", "admin"];
71 assert!(validate_scopes(&requested, allowed).is_ok());
72 }
73
74 #[test]
75 fn validate_scopes_unknown() {
76 let requested = vec!["read".into(), "dangerous".into()];
77 let allowed = &["read", "write"];
78 assert!(validate_scopes(&requested, allowed).is_err());
79 }
80
81 #[test]
82 fn scopes_subset_true() {
83 assert!(scopes_subset(&["a".into()], &["a".into(), "b".into()]));
84 }
85
86 #[test]
87 fn scopes_subset_false() {
88 assert!(!scopes_subset(
89 &["a".into(), "c".into()],
90 &["a".into(), "b".into()]
91 ));
92 }
93}