objectstore_types/
scope.rs1use std::fmt;
9
10const ALLOWED_CHARS: &str =
15 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-()$!+'";
16
17pub const EMPTY_SCOPES: &str = "_";
19
20#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
24pub struct Scope {
25 pub name: String,
29 pub value: String,
33}
34
35impl Scope {
36 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 pub fn name(&self) -> &str {
76 &self.name
77 }
78
79 pub fn value(&self) -> &str {
81 &self.value
82 }
83}
84
85#[derive(Debug, thiserror::Error)]
87pub enum InvalidScopeError {
88 #[error("key and value must be non-empty")]
90 Empty,
91 #[error("invalid character '{0}'")]
93 InvalidChar(char),
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Hash)]
100pub struct Scopes {
101 scopes: Vec<Scope>,
102}
103
104impl Scopes {
105 pub fn empty() -> Self {
107 Self { scopes: vec![] }
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.scopes.is_empty()
113 }
114
115 pub fn get(&self, key: &str) -> Option<&Scope> {
117 self.scopes.iter().find(|s| s.name() == key)
118 }
119
120 pub fn get_value(&self, key: &str) -> Option<&str> {
122 self.get(key).map(|s| s.value())
123 }
124
125 pub fn iter(&self) -> impl Iterator<Item = &Scope> {
127 self.into_iter()
128 }
129
130 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 pub fn as_storage_path(&self) -> AsStoragePath<'_> {
145 AsStoragePath { inner: self }
146 }
147
148 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#[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#[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 #[test]
222 fn test_allowed_characters() {
223 assert!(!ALLOWED_CHARS.contains('.'));
225 assert!(!ALLOWED_CHARS.contains('/'));
226
227 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}