samod_core/storage_key.rs
1use std::fmt;
2
3use automerge::ChangeHash;
4
5use crate::{CompactionHash, DocumentId};
6
7/// A hierarchical key for storage operations in the samod-core system.
8///
9/// `StorageKey` represents a path-like key structure that supports efficient
10/// prefix-based operations. Keys are composed of string components that form
11/// a hierarchy, similar to filesystem paths or namespaces.
12///
13/// ## Usage
14///
15/// Storage keys are used throughout samod-core for organizing data in the
16/// key-value store. They support operations like prefix matching for range
17/// queries and hierarchical organization of related data.
18///
19/// ## Examples
20///
21/// ```rust
22/// use samod_core::StorageKey;
23///
24/// // Create keys from string vectors
25/// let key1 = StorageKey::from_parts(vec!["users", "123", "profile"]).unwrap();
26/// let key2 = StorageKey::from_parts(vec!["users", "123", "settings"]).unwrap();
27/// let prefix = StorageKey::from_parts(vec!["users", "123"]).unwrap();
28///
29/// // Check prefix relationships
30/// assert!(prefix.is_prefix_of(&key1));
31/// assert!(prefix.is_prefix_of(&key2));
32/// ```
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct StorageKey(Vec<String>);
35
36impl StorageKey {
37 pub fn storage_id_path() -> StorageKey {
38 StorageKey(vec!["storage-adapter-id".to_string()])
39 }
40
41 pub fn incremental_prefix(doc_id: &DocumentId) -> StorageKey {
42 StorageKey(vec![doc_id.to_string(), "incremental".to_string()])
43 }
44
45 pub fn incremental_path(doc_id: &DocumentId, change_hash: ChangeHash) -> StorageKey {
46 StorageKey(vec![
47 doc_id.to_string(),
48 "incremental".to_string(),
49 change_hash.to_string(),
50 ])
51 }
52
53 pub fn snapshot_prefix(doc_id: &DocumentId) -> StorageKey {
54 StorageKey(vec![doc_id.to_string(), "snapshot".to_string()])
55 }
56
57 pub fn snapshot_path(doc_id: &DocumentId, compaction_hash: &CompactionHash) -> StorageKey {
58 StorageKey(vec![
59 doc_id.to_string(),
60 "snapshot".to_string(),
61 compaction_hash.to_string(),
62 ])
63 }
64
65 /// Creates a storage key from a slice of string parts.
66 ///
67 /// # Arguments
68 ///
69 /// * `parts` - The parts that make up the key path
70 ///
71 /// # Example
72 ///
73 /// ```rust
74 /// use samod_core::StorageKey;
75 ///
76 /// let key = StorageKey::from_parts(&["users", "123", "profile"]).unwrap();
77 /// ```
78 ///
79 /// # Errors
80 ///
81 /// Returns an error if any part is empty or contains a slash.
82 pub fn from_parts<I: IntoIterator<Item = S>, S: AsRef<str>>(
83 parts: I,
84 ) -> Result<Self, InvalidStorageKey> {
85 let mut components = Vec::new();
86 for part in parts {
87 if part.as_ref().is_empty() || part.as_ref().contains("/") {
88 return Err(InvalidStorageKey);
89 }
90 components.push(part.as_ref().to_string());
91 }
92 Ok(StorageKey(components))
93 }
94
95 /// Checks if this key is a prefix of another key.
96 ///
97 /// # Arguments
98 ///
99 /// * `other` - The key to check against
100 pub fn is_prefix_of(&self, other: &StorageKey) -> bool {
101 if self.0.len() > other.0.len() {
102 return false;
103 }
104 self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b)
105 }
106
107 /// Checks if this key is one level deeper then the given prefix
108 ///
109 /// # Examples
110 ///
111 /// ```rust
112 /// # use samod_core::StorageKey;
113 /// let key = StorageKey::from_parts(vec!["a", "b", "c"]).unwrap();
114 /// let prefix = StorageKey::from_parts(vec!["a", "b"]).unwrap();
115 /// assert_eq!(key.onelevel_deeper(&prefix), Some(StorageKey::from_parts(vec!["a", "b", "c"]).unwrap()));
116 ///
117 /// let prefix2 = StorageKey::from_parts(vec!["a"]).unwrap();
118 /// assert_eq!(key.onelevel_deeper(&prefix2), Some(StorageKey::from_parts(vec!["a", "b"]).unwrap()));
119 ///
120 /// let prefix3 = StorageKey::from_parts(vec!["a", "b", "c", "d"]).unwrap();
121 /// assert_eq!(key.onelevel_deeper(&prefix3), None);
122 /// ```
123 pub fn onelevel_deeper(&self, prefix: &StorageKey) -> Option<StorageKey> {
124 if prefix.is_prefix_of(self) && self.0.len() > prefix.0.len() {
125 let components = self.0.iter().take(prefix.0.len() + 1).cloned();
126 Some(StorageKey(components.collect()))
127 } else {
128 None
129 }
130 }
131
132 pub fn with_suffix(&self, suffix: StorageKey) -> StorageKey {
133 let mut new_key = self.0.clone();
134 new_key.extend(suffix.0);
135 StorageKey(new_key)
136 }
137
138 /// Create a new StorageKey with the given component appended.
139 ///
140 /// # Errors
141 ///
142 /// Returns an error if the new component is empty or contains a forward slash.
143 pub fn with_component(&self, component: String) -> Result<StorageKey, InvalidStorageKey> {
144 if component.is_empty() || component.contains('/') {
145 Err(InvalidStorageKey)
146 } else {
147 let mut new_key = self.0.clone();
148 new_key.push(component);
149 Ok(StorageKey(new_key))
150 }
151 }
152}
153
154impl IntoIterator for StorageKey {
155 type Item = String;
156 type IntoIter = std::vec::IntoIter<String>;
157
158 fn into_iter(self) -> Self::IntoIter {
159 self.0.into_iter()
160 }
161}
162
163impl<'a> IntoIterator for &'a StorageKey {
164 type Item = &'a String;
165 type IntoIter = std::slice::Iter<'a, String>;
166
167 fn into_iter(self) -> Self::IntoIter {
168 self.0.iter()
169 }
170}
171
172impl fmt::Display for StorageKey {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(f, "{}", self.0.join("/"))
175 }
176}
177
178#[derive(Debug)]
179pub struct InvalidStorageKey;
180
181impl std::fmt::Display for InvalidStorageKey {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 write!(f, "InvalidStorageKey")
184 }
185}
186
187impl std::error::Error for InvalidStorageKey {}