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 belongs within the object store.
15///
16/// This is part of the full object identifier, 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: String,
77}
78
79impl ObjectId {
80 /// Creates a new `ObjectId` with the given `context` and `key`.
81 pub fn new(context: ObjectContext, key: String) -> Self {
82 Self::optional(context, Some(key))
83 }
84
85 /// Creates a new `ObjectId` from all of its parts.
86 pub fn from_parts(usecase: String, scopes: Scopes, key: String) -> Self {
87 Self::new(ObjectContext { usecase, scopes }, key)
88 }
89
90 /// Creates a unique `ObjectId` with a random key.
91 ///
92 /// This can be used when creating an object with a server-generated key.
93 pub fn random(context: ObjectContext) -> Self {
94 Self::optional(context, None)
95 }
96
97 /// Creates a new `ObjectId`, generating a key if none is provided.
98 ///
99 /// This creates a unique key like [`ObjectId::random`] if no `key` is provided, or otherwise
100 /// uses the provided `key`.
101 pub fn optional(context: ObjectContext, key: Option<String>) -> Self {
102 Self {
103 context,
104 key: key.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
105 }
106 }
107
108 /// Returns the key of the object.
109 ///
110 /// See [`key`](field@ObjectId::key) for more information.
111 pub fn key(&self) -> &str {
112 &self.key
113 }
114
115 /// Returns the context of the object.
116 ///
117 /// See [`context`](field@ObjectId::context) for more information.
118 pub fn context(&self) -> &ObjectContext {
119 &self.context
120 }
121
122 /// Returns the usecase of the object.
123 ///
124 /// See [`ObjectContext::usecase`] for more information.
125 pub fn usecase(&self) -> &str {
126 &self.context.usecase
127 }
128
129 /// Returns the scopes of the object.
130 ///
131 /// See [`ObjectContext::scopes`] for more information.
132 pub fn scopes(&self) -> &Scopes {
133 &self.context.scopes
134 }
135
136 /// Returns an iterator over all scopes of the object.
137 ///
138 /// See [`ObjectContext::scopes`] for more information.
139 pub fn iter_scopes(&self) -> impl Iterator<Item = &Scope> {
140 self.context.scopes.iter()
141 }
142
143 /// Returns a view that formats this ID as a storage path.
144 ///
145 /// This will format a hierarchical path in the format
146 /// `{usecase}/{scope1.key}.{scope1.value}/.../{key}` that is intended to be used by backends to
147 /// reference the object in a storage system.
148 pub fn as_storage_path(&self) -> AsStoragePath<'_> {
149 AsStoragePath { inner: self }
150 }
151}
152
153/// A view returned by [`ObjectId::as_storage_path`].
154#[derive(Debug)]
155pub struct AsStoragePath<'a> {
156 inner: &'a ObjectId,
157}
158
159impl fmt::Display for AsStoragePath<'_> {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "{}/", self.inner.context.usecase)?;
162 if !self.inner.context.scopes.is_empty() {
163 write!(f, "{}/", self.inner.context.scopes.as_storage_path())?;
164 }
165 write!(f, "objects/{}", self.inner.key)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_storage_path() {
175 let object_id = ObjectId {
176 context: ObjectContext {
177 usecase: "testing".to_string(),
178 scopes: Scopes::from_iter([
179 Scope::create("org", "12345").unwrap(),
180 Scope::create("project", "1337").unwrap(),
181 ]),
182 },
183 key: "foo/bar".to_string(),
184 };
185
186 let path = object_id.as_storage_path().to_string();
187 assert_eq!(path, "testing/org.12345/project.1337/objects/foo/bar");
188 }
189
190 #[test]
191 fn test_storage_path_empty_scopes() {
192 let object_id = ObjectId {
193 context: ObjectContext {
194 usecase: "testing".to_string(),
195 scopes: Scopes::empty(),
196 },
197 key: "foo/bar".to_string(),
198 };
199
200 let path = object_id.as_storage_path().to_string();
201 assert_eq!(path, "testing/objects/foo/bar");
202 }
203}