nodedb_types/id/
collection_key.rs1use super::{DatabaseId, QualifiedCollection};
17
18#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum CollectionKeyError {
22 #[error(
25 "collection name '{name}' is not qualified for database {database_id}; \
26 expected the prefix '{database_id}/'"
27 )]
28 NotQualified {
29 database_id: u64,
31 name: String,
33 },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct CollectionKey<'a> {
50 database_id: DatabaseId,
51 name: &'a str,
52}
53
54impl<'a> CollectionKey<'a> {
55 pub fn from_bare(database_id: DatabaseId, name: &'a str) -> Self {
60 Self { database_id, name }
61 }
62
63 pub fn from_qualified(
65 database_id: DatabaseId,
66 qualified: &'a QualifiedCollection,
67 ) -> Result<Self, CollectionKeyError> {
68 Self::from_qualified_str(database_id, qualified.as_str())
69 }
70
71 pub fn from_qualified_str(
81 database_id: DatabaseId,
82 qualified: &'a str,
83 ) -> Result<Self, CollectionKeyError> {
84 if database_id == DatabaseId::DEFAULT || qualified.is_empty() {
85 return Ok(Self::from_bare(database_id, qualified));
86 }
87 let not_qualified = || CollectionKeyError::NotQualified {
88 database_id: database_id.as_u64(),
89 name: qualified.to_owned(),
90 };
91 let (head, bare) = qualified.split_once('/').ok_or_else(not_qualified)?;
92 if head.is_empty() || !head.bytes().all(|b| b.is_ascii_digit()) {
93 return Err(not_qualified());
94 }
95 match head.parse::<u64>() {
96 Ok(raw) if raw == database_id.as_u64() && !head.starts_with('0') => {
97 Ok(Self::from_bare(database_id, bare))
98 }
99 _ => Err(not_qualified()),
100 }
101 }
102
103 pub fn database_id(&self) -> DatabaseId {
105 self.database_id
106 }
107
108 pub fn name(&self) -> &'a str {
110 self.name
111 }
112
113 pub fn qualified(&self) -> QualifiedCollection {
115 QualifiedCollection::new(self.database_id, self.name)
116 }
117
118 pub fn vshard(&self) -> super::VShardId {
120 super::VShardId::from_collection(*self)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 const DB: DatabaseId = DatabaseId::new(1024);
129
130 #[test]
131 fn qualified_input_yields_the_bare_key() {
132 let qualified = QualifiedCollection::new(DB, "users");
133 let from_qualified = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
134 assert_eq!(from_qualified.name(), "users");
135 assert_eq!(from_qualified, CollectionKey::from_bare(DB, "users"));
136 assert_eq!(
137 from_qualified.vshard(),
138 CollectionKey::from_bare(DB, "users").vshard()
139 );
140 }
141
142 #[test]
143 fn qualified_string_cannot_become_a_key_without_dequalifying() {
144 let qualified = QualifiedCollection::new(DB, "users");
149 let key = CollectionKey::from_qualified_str(DB, qualified.as_str()).expect("qualified");
150 assert_ne!(key.name(), qualified.as_str());
151 assert_eq!(key.name(), "users");
152 assert_eq!(key.qualified(), qualified);
153 }
154
155 #[test]
156 fn default_database_names_are_already_bare() {
157 let key = CollectionKey::from_qualified_str(DatabaseId::DEFAULT, "users").expect("default");
158 assert_eq!(key, CollectionKey::from_bare(DatabaseId::DEFAULT, "users"));
159 }
160
161 #[test]
162 fn empty_name_is_the_no_collection_sentinel_in_every_database() {
163 let key = CollectionKey::from_qualified_str(DB, "").expect("empty");
164 assert_eq!(key, CollectionKey::from_bare(DB, ""));
165 }
166
167 #[test]
168 fn exactly_one_qualifier_is_stripped() {
169 let qualified = QualifiedCollection::new(DB, "1024/nested");
170 let key = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
171 assert_eq!(key.name(), "1024/nested");
172 }
173
174 #[test]
175 fn unqualified_name_in_a_named_database_is_rejected() {
176 let err = CollectionKey::from_qualified_str(DB, "users").expect_err("unqualified");
177 assert_eq!(
178 err,
179 CollectionKeyError::NotQualified {
180 database_id: 1024,
181 name: "users".to_owned(),
182 }
183 );
184 }
185
186 #[test]
187 fn foreign_database_qualifier_is_rejected() {
188 assert!(CollectionKey::from_qualified_str(DB, "7/users").is_err());
189 assert!(CollectionKey::from_qualified_str(DB, "+1024/users").is_err());
190 assert!(CollectionKey::from_qualified_str(DB, "01024/users").is_err());
191 assert!(CollectionKey::from_qualified_str(DB, "/users").is_err());
192 }
193
194 #[test]
195 fn bare_and_qualified_hashes_differ_for_a_named_database() {
196 let mut differs = false;
199 for i in 0..64 {
200 let name = format!("coll_{i}");
201 let qualified = QualifiedCollection::new(DB, &name);
202 let key = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
203 let raw_qualified = CollectionKey::from_bare(DB, qualified.as_str());
204 assert_eq!(key.vshard(), CollectionKey::from_bare(DB, &name).vshard());
205 differs |= key.vshard() != raw_qualified.vshard();
206 }
207 assert!(differs);
208 }
209}