objectstore_service/
id.rs

1//! Definitions for object identifiers, including usecases and scopes.
2//!
3//! This module contains types to define and manage object identifiers:
4//!
5//!  - [`ObjectId`] is the main identifier type for objects, consisting of a usecase, scopes, and a
6//!    key. Every object stored in the object store has a unique `ObjectId`.
7//!  - [`Scope`] and [`Scopes`] define hierarchical scopes for objects, which are part of the
8//!    `ObjectId`.
9
10use std::fmt;
11
12use objectstore_types::scope::{Scope, Scopes};
13
14/// Defines where an object, or batch of objects, belongs within the object store.
15///
16/// This is part of the full object identifier for single objects, see [`ObjectId`].
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct ObjectContext {
19    /// The usecase, or "product" this object belongs to.
20    ///
21    /// This can be defined on-the-fly by the client, but special server logic
22    /// (such as the concrete backend/bucket) can be tied to this as well.
23    pub usecase: String,
24
25    /// The scopes of the object, used for compartmentalization and authorization.
26    ///
27    /// Scopes are hierarchical key-value pairs that act as containers for objects. The first,
28    /// top-level scope can contain sub scopes, like a structured nested folder system. As such,
29    /// scopes are used for isolation and access authorization.
30    ///
31    /// # Ordering
32    ///
33    /// Note that the order of scopes matters! For example, `organization=17,project=42` indicates
34    /// that project _42_ is part of organization _17_. If an object were created with these scopes
35    /// reversed, it counts as a different object.
36    ///
37    /// Not every object within a usecase needs to have the same scopes. It is perfectly valid to
38    /// create objects with disjunct or a subset of scopes. However, by convention, we recommend to
39    /// use the same scopes for all objects within a usecase where possible.
40    ///
41    /// # Creation
42    ///
43    /// To create scopes, collect from an iterator of [`Scope`]s. Since scopes must be validated,
44    /// you must use [`Scope::create`] to create them:
45    ///
46    /// ```
47    /// use objectstore_service::id::ObjectContext;
48    /// use objectstore_types::scope::{Scope, Scopes};
49    ///
50    /// let object_id = ObjectContext {
51    ///     usecase: "my_usecase".to_string(),
52    ///     scopes: Scopes::from_iter([
53    ///         Scope::create("organization", "17").unwrap(),
54    ///         Scope::create("project", "42").unwrap(),
55    ///     ]),
56    /// };
57    /// ```
58    pub scopes: Scopes,
59}
60
61/// The fully qualified identifier of an object.
62///
63/// This consists of a usecase and the scopes, which make up the object's context and define where
64/// the object belongs within objectstore, as well as the unique key within the context.
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct ObjectId {
67    /// The usecase and scopes this object belongs to.
68    pub context: ObjectContext,
69
70    /// This key uniquely identifies the object within its usecase and scopes.
71    ///
72    /// Note that keys can be reused across different contexts. Only in combination with the context
73    /// a key makes a unique identifier.
74    ///
75    /// Keys can be assigned by the service. For this, use [`ObjectId::random`].
76    pub key: ObjectKey,
77}
78
79/// A key that uniquely identifies an object within its usecase and scopes.
80pub type ObjectKey = String;
81
82impl ObjectId {
83    /// Creates a new `ObjectId` with the given `context` and `key`.
84    pub fn new(context: ObjectContext, key: String) -> Self {
85        Self::optional(context, Some(key))
86    }
87
88    /// Creates a new `ObjectId` from all of its parts.
89    pub fn from_parts(usecase: String, scopes: Scopes, key: String) -> Self {
90        Self::new(ObjectContext { usecase, scopes }, key)
91    }
92
93    /// Creates a unique `ObjectId` with a random key.
94    ///
95    /// This can be used when creating an object with a server-generated key.
96    pub fn random(context: ObjectContext) -> Self {
97        Self::optional(context, None)
98    }
99
100    /// Creates a new `ObjectId`, generating a key if none is provided.
101    ///
102    /// This creates a unique key like [`ObjectId::random`] if no `key` is provided, or otherwise
103    /// uses the provided `key`.
104    pub fn optional(context: ObjectContext, key: Option<String>) -> Self {
105        Self {
106            context,
107            key: key.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
108        }
109    }
110
111    /// Returns the key of the object.
112    ///
113    /// See [`key`](field@ObjectId::key) for more information.
114    pub fn key(&self) -> &str {
115        &self.key
116    }
117
118    /// Returns the context of the object.
119    ///
120    /// See [`context`](field@ObjectId::context) for more information.
121    pub fn context(&self) -> &ObjectContext {
122        &self.context
123    }
124
125    /// Returns the usecase of the object.
126    ///
127    /// See [`ObjectContext::usecase`] for more information.
128    pub fn usecase(&self) -> &str {
129        &self.context.usecase
130    }
131
132    /// Returns the scopes of the object.
133    ///
134    /// See [`ObjectContext::scopes`] for more information.
135    pub fn scopes(&self) -> &Scopes {
136        &self.context.scopes
137    }
138
139    /// Returns an iterator over all scopes of the object.
140    ///
141    /// See [`ObjectContext::scopes`] for more information.
142    pub fn iter_scopes(&self) -> impl Iterator<Item = &Scope> {
143        self.context.scopes.iter()
144    }
145
146    /// Returns a view that formats this ID as a storage path.
147    ///
148    /// This will format a hierarchical path in the format
149    /// `{usecase}/{scope1.key}.{scope1.value}/.../{key}` that is intended to be used by backends to
150    /// reference the object in a storage system.
151    pub fn as_storage_path(&self) -> AsStoragePath<'_> {
152        AsStoragePath { inner: self }
153    }
154}
155
156/// A view returned by [`ObjectId::as_storage_path`].
157#[derive(Debug)]
158pub struct AsStoragePath<'a> {
159    inner: &'a ObjectId,
160}
161
162impl fmt::Display for AsStoragePath<'_> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}/", self.inner.context.usecase)?;
165        if !self.inner.context.scopes.is_empty() {
166            write!(f, "{}/", self.inner.context.scopes.as_storage_path())?;
167        }
168        write!(f, "objects/{}", self.inner.key)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_storage_path() {
178        let object_id = ObjectId {
179            context: ObjectContext {
180                usecase: "testing".to_string(),
181                scopes: Scopes::from_iter([
182                    Scope::create("org", "12345").unwrap(),
183                    Scope::create("project", "1337").unwrap(),
184                ]),
185            },
186            key: "foo/bar".to_string(),
187        };
188
189        let path = object_id.as_storage_path().to_string();
190        assert_eq!(path, "testing/org.12345/project.1337/objects/foo/bar");
191    }
192
193    #[test]
194    fn test_storage_path_empty_scopes() {
195        let object_id = ObjectId {
196            context: ObjectContext {
197                usecase: "testing".to_string(),
198                scopes: Scopes::empty(),
199            },
200            key: "foo/bar".to_string(),
201        };
202
203        let path = object_id.as_storage_path().to_string();
204        assert_eq!(path, "testing/objects/foo/bar");
205    }
206}