objectstore_types/
scope.rs1use std::fmt;
46
47const ALLOWED_CHARS: &str =
52 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-()$!+'";
53
54pub const EMPTY_SCOPES: &str = "_";
56
57#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
62pub struct Scope {
63 pub name: String,
67 pub value: String,
71}
72
73impl Scope {
74 pub fn create<V>(name: &str, value: V) -> Result<Self, InvalidScopeError>
92 where
93 V: fmt::Display,
94 {
95 let value = value.to_string();
96 if name.is_empty() || value.is_empty() {
97 return Err(InvalidScopeError::Empty);
98 }
99
100 for c in name.chars().chain(value.chars()) {
101 if !ALLOWED_CHARS.contains(c) {
102 return Err(InvalidScopeError::InvalidChar(c));
103 }
104 }
105
106 Ok(Self {
107 name: name.to_owned(),
108 value,
109 })
110 }
111
112 pub fn name(&self) -> &str {
114 &self.name
115 }
116
117 pub fn value(&self) -> &str {
119 &self.value
120 }
121}
122
123#[derive(Clone, Debug, thiserror::Error)]
125pub enum InvalidScopeError {
126 #[error("key and value must be non-empty")]
128 Empty,
129 #[error("invalid character '{0}'")]
131 InvalidChar(char),
132 #[error("unexpected error, please report a bug")]
136 Unreachable,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, Hash)]
144pub struct Scopes {
145 scopes: Vec<Scope>,
146}
147
148impl Scopes {
149 pub fn empty() -> Self {
151 Self { scopes: vec![] }
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.scopes.is_empty()
157 }
158
159 pub fn get(&self, key: &str) -> Option<&Scope> {
161 self.scopes.iter().find(|s| s.name() == key)
162 }
163
164 pub fn get_value(&self, key: &str) -> Option<&str> {
166 self.get(key).map(|s| s.value())
167 }
168
169 pub fn iter(&self) -> impl Iterator<Item = &Scope> {
171 self.into_iter()
172 }
173
174 pub fn push<V>(&mut self, key: &str, value: V) -> Result<(), InvalidScopeError>
176 where
177 V: fmt::Display,
178 {
179 self.scopes.push(Scope::create(key, value)?);
180 Ok(())
181 }
182
183 pub fn as_storage_path(&self) -> AsStoragePath<'_> {
189 AsStoragePath { inner: self }
190 }
191
192 pub fn as_api_path(&self) -> AsApiPath<'_> {
197 AsApiPath { inner: self }
198 }
199}
200
201impl<'a> IntoIterator for &'a Scopes {
202 type IntoIter = std::slice::Iter<'a, Scope>;
203 type Item = &'a Scope;
204
205 fn into_iter(self) -> Self::IntoIter {
206 self.scopes.iter()
207 }
208}
209
210impl FromIterator<Scope> for Scopes {
211 fn from_iter<T>(iter: T) -> Self
212 where
213 T: IntoIterator<Item = Scope>,
214 {
215 Self {
216 scopes: iter.into_iter().collect(),
217 }
218 }
219}
220
221#[derive(Debug)]
223pub struct AsStoragePath<'a> {
224 inner: &'a Scopes,
225}
226
227impl fmt::Display for AsStoragePath<'_> {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 for (i, scope) in self.inner.iter().enumerate() {
230 if i > 0 {
231 write!(f, "/")?;
232 }
233 write!(f, "{}.{}", scope.name, scope.value)?;
234 }
235 Ok(())
236 }
237}
238
239#[derive(Debug)]
241pub struct AsApiPath<'a> {
242 inner: &'a Scopes,
243}
244
245impl fmt::Display for AsApiPath<'_> {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 if let Some((first, rest)) = self.inner.scopes.split_first() {
248 write!(f, "{}={}", first.name, first.value)?;
249 for scope in rest {
250 write!(f, ";{}={}", scope.name, scope.value)?;
251 }
252 Ok(())
253 } else {
254 f.write_str(EMPTY_SCOPES)
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
266 fn test_allowed_characters() {
267 assert!(!ALLOWED_CHARS.contains('.'));
269 assert!(!ALLOWED_CHARS.contains('/'));
270
271 assert!(!ALLOWED_CHARS.contains('='));
273 assert!(!ALLOWED_CHARS.contains(';'));
274 }
275
276 #[test]
277 fn test_create_scope_empty() {
278 let err = Scope::create("", "value").unwrap_err();
279 assert!(matches!(err, InvalidScopeError::Empty));
280
281 let err = Scope::create("key", "").unwrap_err();
282 assert!(matches!(err, InvalidScopeError::Empty));
283 }
284
285 #[test]
286 fn test_create_scope_invalid_char() {
287 let err = Scope::create("key/", "value").unwrap_err();
288 assert!(matches!(err, InvalidScopeError::InvalidChar('/')));
289
290 let err = dbg!(Scope::create("key", "⚠️").unwrap_err());
291 assert!(matches!(err, InvalidScopeError::InvalidChar('⚠')));
292 }
293
294 #[test]
295 fn test_as_storage_path() {
296 let scopes = Scopes::from_iter([
297 Scope::create("org", "12345").unwrap(),
298 Scope::create("project", "1337").unwrap(),
299 ]);
300
301 let storage_path = scopes.as_storage_path().to_string();
302 assert_eq!(storage_path, "org.12345/project.1337");
303
304 let empty_scopes = Scopes::empty();
305 let storage_path = empty_scopes.as_storage_path().to_string();
306 assert_eq!(storage_path, "");
307 }
308
309 #[test]
310 fn test_as_api_path() {
311 let scopes = Scopes::from_iter([
312 Scope::create("org", "12345").unwrap(),
313 Scope::create("project", "1337").unwrap(),
314 ]);
315
316 let api_path = scopes.as_api_path().to_string();
317 assert_eq!(api_path, "org=12345;project=1337");
318
319 let empty_scopes = Scopes::empty();
320 let api_path = empty_scopes.as_api_path().to_string();
321 assert_eq!(api_path, EMPTY_SCOPES);
322 }
323
324 #[test]
325 fn test_push_and_get() {
326 let mut scopes = Scopes::empty();
327 scopes.push("org", "123").unwrap();
328 scopes.push("project", "456").unwrap();
329
330 assert_eq!(scopes.get("org").unwrap().value(), "123");
331 assert_eq!(scopes.get("project").unwrap().value(), "456");
332 assert!(scopes.get("missing").is_none());
333 }
334
335 #[test]
336 fn test_get_value() {
337 let mut scopes = Scopes::empty();
338 scopes.push("org", "123").unwrap();
339
340 assert_eq!(scopes.get_value("org"), Some("123"));
341 assert_eq!(scopes.get_value("missing"), None);
342 }
343
344 #[test]
345 fn test_push_validates() {
346 let mut scopes = Scopes::empty();
347 assert!(scopes.push("", "value").is_err());
348 assert!(scopes.push("key", "").is_err());
349 assert!(scopes.push("key/bad", "value").is_err());
350 assert!(scopes.is_empty());
351 }
352
353 #[test]
354 fn test_is_empty() {
355 let empty = Scopes::empty();
356 assert!(empty.is_empty());
357
358 let mut non_empty = Scopes::empty();
359 non_empty.push("org", "1").unwrap();
360 assert!(!non_empty.is_empty());
361 }
362}