1use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct Scopes {
12 values: BTreeSet<String>,
13}
14
15impl Scopes {
16 pub fn new() -> Self {
17 Scopes::default()
18 }
19
20 pub fn parse(raw: &str) -> Scopes {
22 Scopes {
23 values: raw
24 .split_whitespace()
25 .filter(|scope| !scope.is_empty())
26 .map(str::to_string)
27 .collect(),
28 }
29 }
30
31 pub fn of<I, S>(scopes: I) -> Scopes
32 where
33 I: IntoIterator<Item = S>,
34 S: Into<String>,
35 {
36 Scopes { values: scopes.into_iter().map(Into::into).collect() }
37 }
38
39 pub fn add(&mut self, scope: impl Into<String>) {
40 self.values.insert(scope.into());
41 }
42
43 pub fn with(mut self, scope: impl Into<String>) -> Self {
44 self.add(scope);
45 self
46 }
47
48 pub fn contains(&self, scope: &str) -> bool {
49 self.values.contains(scope)
50 }
51
52 pub fn covers(&self, required: &Scopes) -> bool {
57 required.values.is_subset(&self.values)
58 }
59
60 pub fn intersect(&self, allowed: &Scopes) -> Scopes {
67 Scopes { values: self.values.intersection(&allowed.values).cloned().collect() }
68 }
69
70 pub fn beyond(&self, allowed: &Scopes) -> Scopes {
72 Scopes { values: self.values.difference(&allowed.values).cloned().collect() }
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.values.is_empty()
77 }
78
79 pub fn len(&self) -> usize {
80 self.values.len()
81 }
82
83 pub fn iter(&self) -> impl Iterator<Item = &str> {
84 self.values.iter().map(String::as_str)
85 }
86}
87
88impl std::fmt::Display for Scopes {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 let joined: Vec<&str> = self.values.iter().map(String::as_str).collect();
91 f.write_str(&joined.join(" "))
92 }
93}
94
95impl From<&str> for Scopes {
96 fn from(raw: &str) -> Scopes {
97 Scopes::parse(raw)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn parses_the_wire_form_forgivingly() {
107 let scopes = Scopes::parse(" read write read ");
108
109 assert_eq!(scopes.len(), 2, "duplicates and blanks are dropped");
110 assert!(scopes.contains("read"));
111 assert!(scopes.contains("write"));
112 }
113
114 #[test]
115 fn renders_in_a_stable_order() {
116 assert_eq!(Scopes::of(["write", "read"]).to_string(), "read write");
119 assert_eq!(Scopes::parse("write read").to_string(), "read write");
120 }
121
122 #[test]
123 fn an_empty_set_is_an_empty_string() {
124 assert_eq!(Scopes::new().to_string(), "");
125 assert!(Scopes::parse(" ").is_empty());
126 }
127
128 #[test]
129 fn coverage_asks_whether_what_was_granted_is_enough() {
130 let granted = Scopes::of(["read", "write", "delete"]);
131
132 assert!(granted.covers(&Scopes::of(["read"])));
133 assert!(granted.covers(&Scopes::of(["read", "write"])));
134 assert!(granted.covers(&Scopes::new()), "asking for nothing is always covered");
135 assert!(!granted.covers(&Scopes::of(["admin"])));
136 }
137
138 #[test]
139 fn a_request_can_be_narrowed_to_what_a_client_may_have() {
140 let asked = Scopes::of(["read", "write", "admin"]);
141 let allowed = Scopes::of(["read", "write"]);
142
143 assert_eq!(asked.intersect(&allowed), Scopes::of(["read", "write"]));
144 assert_eq!(asked.beyond(&allowed), Scopes::of(["admin"]));
145 }
146
147 #[test]
148 fn round_trips_through_the_wire_form() {
149 let original = Scopes::of(["openid", "profile", "email"]);
150 assert_eq!(Scopes::parse(&original.to_string()), original);
151 }
152}