objectstore_types/
scope.rs

1//! Definitions for object scops.
2//!
3//! This module contains types to define and manage the hierarchical organization of objects:
4//!
5//!  - [`Scope`] is a single key-value pair representing one level of hierarchy
6//!  - [`Scopes`] is an ordered collection of [`Scope`]s
7
8use std::fmt;
9
10/// Characters allowed in a Scope's key and value.
11///
12/// These are the URL safe characters, except for `.` which we use as separator between
13/// key and value of Scope components in backends.
14const ALLOWED_CHARS: &str =
15    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-()$!+'";
16
17/// Used in place of scopes in the URL to represent an empty set of scopes.
18pub const EMPTY_SCOPES: &str = "_";
19
20/// A single scope value of an object.
21///
22/// Scopes are used in a hierarchy in object IDs.
23#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
24pub struct Scope {
25    /// Identifies the scope.
26    ///
27    /// Examples are `organization` or `project`.
28    pub name: String,
29    /// The value of the scope.
30    ///
31    /// This can be the identifier of a
32    pub value: String,
33}
34
35impl Scope {
36    /// Creates and validates a new scope.
37    ///
38    /// The name and value must be non-empty.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use objectstore_types::scope::Scope;
44    ///
45    /// let scope = Scope::create("organization", "17").unwrap();
46    /// assert_eq!(scope.name(), "organization");
47    /// assert_eq!(scope.value(), "17");
48    ///
49    /// // Empty names or values are invalid
50    /// let invalid_scope = Scope::create("", "value");
51    /// assert!(invalid_scope.is_err());
52    /// ```
53    pub fn create<V>(name: &str, value: V) -> Result<Self, InvalidScopeError>
54    where
55        V: fmt::Display,
56    {
57        let value = value.to_string();
58        if name.is_empty() || value.is_empty() {
59            return Err(InvalidScopeError::Empty);
60        }
61
62        for c in name.chars().chain(value.chars()) {
63            if !ALLOWED_CHARS.contains(c) {
64                return Err(InvalidScopeError::InvalidChar(c));
65            }
66        }
67
68        Ok(Self {
69            name: name.to_owned(),
70            value,
71        })
72    }
73
74    /// Returns the name of the scope.
75    pub fn name(&self) -> &str {
76        &self.name
77    }
78
79    /// Returns the value of the scope.
80    pub fn value(&self) -> &str {
81        &self.value
82    }
83}
84
85/// An error indicating that a scope is invalid, returned by [`Scope::create`].
86#[derive(Debug, thiserror::Error)]
87pub enum InvalidScopeError {
88    /// Indicates that either the key or value is empty.
89    #[error("key and value must be non-empty")]
90    Empty,
91    /// Indicates that the key or value contains an invalid character.
92    #[error("invalid character '{0}'")]
93    InvalidChar(char),
94}
95
96/// An ordered set of resource scopes.
97///
98/// Scopes are used to create hierarchical identifiers for objects.
99#[derive(Clone, Debug, PartialEq, Eq, Hash)]
100pub struct Scopes {
101    scopes: Vec<Scope>,
102}
103
104impl Scopes {
105    /// Returns an empty set of scopes.
106    pub fn empty() -> Self {
107        Self { scopes: vec![] }
108    }
109
110    /// Returns `true` if there are no scopes.
111    pub fn is_empty(&self) -> bool {
112        self.scopes.is_empty()
113    }
114
115    /// Returns the scope with the given key, if it exists.
116    pub fn get(&self, key: &str) -> Option<&Scope> {
117        self.scopes.iter().find(|s| s.name() == key)
118    }
119
120    /// Returns the value of the scope with the given key, if it exists.
121    pub fn get_value(&self, key: &str) -> Option<&str> {
122        self.get(key).map(|s| s.value())
123    }
124
125    /// Returns an iterator over all scopes.
126    pub fn iter(&self) -> impl Iterator<Item = &Scope> {
127        self.into_iter()
128    }
129
130    /// Pushes a new scope to the collection.
131    pub fn push<V>(&mut self, key: &str, value: V) -> Result<(), InvalidScopeError>
132    where
133        V: fmt::Display,
134    {
135        self.scopes.push(Scope::create(key, value)?);
136        Ok(())
137    }
138
139    /// Returns a view that formats the scopes as path for storage.
140    ///
141    /// This will serialize the scopes as `{name}.{value}/...`, which is intended to be used by
142    /// backends to reference the object in a storage system. This becomes part of the storage path
143    /// of an `ObjectId`.
144    pub fn as_storage_path(&self) -> AsStoragePath<'_> {
145        AsStoragePath { inner: self }
146    }
147
148    /// Returns a view that formats the scopes as path for web API usage.
149    ///
150    /// This will serialize the scopes as `{name}={value};...`, which is intended to be used by
151    /// clients to format URL paths.
152    pub fn as_api_path(&self) -> AsApiPath<'_> {
153        AsApiPath { inner: self }
154    }
155}
156
157impl<'a> IntoIterator for &'a Scopes {
158    type IntoIter = std::slice::Iter<'a, Scope>;
159    type Item = &'a Scope;
160
161    fn into_iter(self) -> Self::IntoIter {
162        self.scopes.iter()
163    }
164}
165
166impl FromIterator<Scope> for Scopes {
167    fn from_iter<T>(iter: T) -> Self
168    where
169        T: IntoIterator<Item = Scope>,
170    {
171        Self {
172            scopes: iter.into_iter().collect(),
173        }
174    }
175}
176
177/// A view returned by [`Scopes::as_storage_path`].
178#[derive(Debug)]
179pub struct AsStoragePath<'a> {
180    inner: &'a Scopes,
181}
182
183impl fmt::Display for AsStoragePath<'_> {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        for (i, scope) in self.inner.iter().enumerate() {
186            if i > 0 {
187                write!(f, "/")?;
188            }
189            write!(f, "{}.{}", scope.name, scope.value)?;
190        }
191        Ok(())
192    }
193}
194
195/// A view returned by [`Scopes::as_api_path`].
196#[derive(Debug)]
197pub struct AsApiPath<'a> {
198    inner: &'a Scopes,
199}
200
201impl fmt::Display for AsApiPath<'_> {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        if let Some((first, rest)) = self.inner.scopes.split_first() {
204            write!(f, "{}={}", first.name, first.value)?;
205            for scope in rest {
206                write!(f, ";{}={}", scope.name, scope.value)?;
207            }
208            Ok(())
209        } else {
210            f.write_str(EMPTY_SCOPES)
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    /// Regression test to ensure we're not unintentionally adding characters to the allowed set
220    /// that are required in storage or API paths.
221    #[test]
222    fn test_allowed_characters() {
223        // Storage paths
224        assert!(!ALLOWED_CHARS.contains('.'));
225        assert!(!ALLOWED_CHARS.contains('/'));
226
227        // API paths
228        assert!(!ALLOWED_CHARS.contains('='));
229        assert!(!ALLOWED_CHARS.contains(';'));
230    }
231
232    #[test]
233    fn test_create_scope_empty() {
234        let err = Scope::create("", "value").unwrap_err();
235        assert!(matches!(err, InvalidScopeError::Empty));
236
237        let err = Scope::create("key", "").unwrap_err();
238        assert!(matches!(err, InvalidScopeError::Empty));
239    }
240
241    #[test]
242    fn test_create_scope_invalid_char() {
243        let err = Scope::create("key/", "value").unwrap_err();
244        assert!(matches!(err, InvalidScopeError::InvalidChar('/')));
245
246        let err = dbg!(Scope::create("key", "⚠️").unwrap_err());
247        assert!(matches!(err, InvalidScopeError::InvalidChar('⚠')));
248    }
249
250    #[test]
251    fn test_as_storage_path() {
252        let scopes = Scopes::from_iter([
253            Scope::create("org", "12345").unwrap(),
254            Scope::create("project", "1337").unwrap(),
255        ]);
256
257        let storage_path = scopes.as_storage_path().to_string();
258        assert_eq!(storage_path, "org.12345/project.1337");
259
260        let empty_scopes = Scopes::empty();
261        let storage_path = empty_scopes.as_storage_path().to_string();
262        assert_eq!(storage_path, "");
263    }
264
265    #[test]
266    fn test_as_api_path() {
267        let scopes = Scopes::from_iter([
268            Scope::create("org", "12345").unwrap(),
269            Scope::create("project", "1337").unwrap(),
270        ]);
271
272        let api_path = scopes.as_api_path().to_string();
273        assert_eq!(api_path, "org=12345;project=1337");
274
275        let empty_scopes = Scopes::empty();
276        let api_path = empty_scopes.as_api_path().to_string();
277        assert_eq!(api_path, EMPTY_SCOPES);
278    }
279}