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")]
135 Unreachable,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, Hash)]
143pub struct Scopes {
144 scopes: Vec<Scope>,
145}
146
147impl Scopes {
148 pub fn empty() -> Self {
150 Self { scopes: vec![] }
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.scopes.is_empty()
156 }
157
158 pub fn get(&self, key: &str) -> Option<&Scope> {
160 self.scopes.iter().find(|s| s.name() == key)
161 }
162
163 pub fn get_value(&self, key: &str) -> Option<&str> {
165 self.get(key).map(|s| s.value())
166 }
167
168 pub fn iter(&self) -> impl Iterator<Item = &Scope> {
170 self.into_iter()
171 }
172
173 pub fn push<V>(&mut self, key: &str, value: V) -> Result<(), InvalidScopeError>
175 where
176 V: fmt::Display,
177 {
178 self.scopes.push(Scope::create(key, value)?);
179 Ok(())
180 }
181
182 pub fn as_storage_path(&self) -> AsStoragePath<'_> {
188 AsStoragePath { inner: self }
189 }
190
191 pub fn as_api_path(&self) -> AsApiPath<'_> {
196 AsApiPath { inner: self }
197 }
198}
199
200impl<'a> IntoIterator for &'a Scopes {
201 type IntoIter = std::slice::Iter<'a, Scope>;
202 type Item = &'a Scope;
203
204 fn into_iter(self) -> Self::IntoIter {
205 self.scopes.iter()
206 }
207}
208
209impl FromIterator<Scope> for Scopes {
210 fn from_iter<T>(iter: T) -> Self
211 where
212 T: IntoIterator<Item = Scope>,
213 {
214 Self {
215 scopes: iter.into_iter().collect(),
216 }
217 }
218}
219
220#[derive(Debug)]
222pub struct AsStoragePath<'a> {
223 inner: &'a Scopes,
224}
225
226impl fmt::Display for AsStoragePath<'_> {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 for (i, scope) in self.inner.iter().enumerate() {
229 if i > 0 {
230 write!(f, "/")?;
231 }
232 write!(f, "{}.{}", scope.name, scope.value)?;
233 }
234 Ok(())
235 }
236}
237
238#[derive(Debug)]
240pub struct AsApiPath<'a> {
241 inner: &'a Scopes,
242}
243
244impl fmt::Display for AsApiPath<'_> {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 if let Some((first, rest)) = self.inner.scopes.split_first() {
247 write!(f, "{}={}", first.name, first.value)?;
248 for scope in rest {
249 write!(f, ";{}={}", scope.name, scope.value)?;
250 }
251 Ok(())
252 } else {
253 f.write_str(EMPTY_SCOPES)
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 fn test_allowed_characters() {
266 assert!(!ALLOWED_CHARS.contains('.'));
268 assert!(!ALLOWED_CHARS.contains('/'));
269
270 assert!(!ALLOWED_CHARS.contains('='));
272 assert!(!ALLOWED_CHARS.contains(';'));
273 }
274
275 #[test]
276 fn test_create_scope_empty() {
277 let err = Scope::create("", "value").unwrap_err();
278 assert!(matches!(err, InvalidScopeError::Empty));
279
280 let err = Scope::create("key", "").unwrap_err();
281 assert!(matches!(err, InvalidScopeError::Empty));
282 }
283
284 #[test]
285 fn test_create_scope_invalid_char() {
286 let err = Scope::create("key/", "value").unwrap_err();
287 assert!(matches!(err, InvalidScopeError::InvalidChar('/')));
288
289 let err = dbg!(Scope::create("key", "⚠️").unwrap_err());
290 assert!(matches!(err, InvalidScopeError::InvalidChar('⚠')));
291 }
292
293 #[test]
294 fn test_as_storage_path() {
295 let scopes = Scopes::from_iter([
296 Scope::create("org", "12345").unwrap(),
297 Scope::create("project", "1337").unwrap(),
298 ]);
299
300 let storage_path = scopes.as_storage_path().to_string();
301 assert_eq!(storage_path, "org.12345/project.1337");
302
303 let empty_scopes = Scopes::empty();
304 let storage_path = empty_scopes.as_storage_path().to_string();
305 assert_eq!(storage_path, "");
306 }
307
308 #[test]
309 fn test_as_api_path() {
310 let scopes = Scopes::from_iter([
311 Scope::create("org", "12345").unwrap(),
312 Scope::create("project", "1337").unwrap(),
313 ]);
314
315 let api_path = scopes.as_api_path().to_string();
316 assert_eq!(api_path, "org=12345;project=1337");
317
318 let empty_scopes = Scopes::empty();
319 let api_path = empty_scopes.as_api_path().to_string();
320 assert_eq!(api_path, EMPTY_SCOPES);
321 }
322
323 #[test]
324 fn test_push_and_get() {
325 let mut scopes = Scopes::empty();
326 scopes.push("org", "123").unwrap();
327 scopes.push("project", "456").unwrap();
328
329 assert_eq!(scopes.get("org").unwrap().value(), "123");
330 assert_eq!(scopes.get("project").unwrap().value(), "456");
331 assert!(scopes.get("missing").is_none());
332 }
333
334 #[test]
335 fn test_get_value() {
336 let mut scopes = Scopes::empty();
337 scopes.push("org", "123").unwrap();
338
339 assert_eq!(scopes.get_value("org"), Some("123"));
340 assert_eq!(scopes.get_value("missing"), None);
341 }
342
343 #[test]
344 fn test_push_validates() {
345 let mut scopes = Scopes::empty();
346 assert!(scopes.push("", "value").is_err());
347 assert!(scopes.push("key", "").is_err());
348 assert!(scopes.push("key/bad", "value").is_err());
349 assert!(scopes.is_empty());
350 }
351
352 #[test]
353 fn test_is_empty() {
354 let empty = Scopes::empty();
355 assert!(empty.is_empty());
356
357 let mut non_empty = Scopes::empty();
358 non_empty.push("org", "1").unwrap();
359 assert!(!non_empty.is_empty());
360 }
361}