use super::{DatabaseId, QualifiedCollection};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CollectionKeyError {
#[error(
"collection name '{name}' is not qualified for database {database_id}; \
expected the prefix '{database_id}/'"
)]
NotQualified {
database_id: u64,
name: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CollectionKey<'a> {
database_id: DatabaseId,
name: &'a str,
}
impl<'a> CollectionKey<'a> {
pub fn from_bare(database_id: DatabaseId, name: &'a str) -> Self {
Self { database_id, name }
}
pub fn from_qualified(
database_id: DatabaseId,
qualified: &'a QualifiedCollection,
) -> Result<Self, CollectionKeyError> {
Self::from_qualified_str(database_id, qualified.as_str())
}
pub fn from_qualified_str(
database_id: DatabaseId,
qualified: &'a str,
) -> Result<Self, CollectionKeyError> {
if database_id == DatabaseId::DEFAULT || qualified.is_empty() {
return Ok(Self::from_bare(database_id, qualified));
}
let not_qualified = || CollectionKeyError::NotQualified {
database_id: database_id.as_u64(),
name: qualified.to_owned(),
};
let (head, bare) = qualified.split_once('/').ok_or_else(not_qualified)?;
if head.is_empty() || !head.bytes().all(|b| b.is_ascii_digit()) {
return Err(not_qualified());
}
match head.parse::<u64>() {
Ok(raw) if raw == database_id.as_u64() && !head.starts_with('0') => {
Ok(Self::from_bare(database_id, bare))
}
_ => Err(not_qualified()),
}
}
pub fn database_id(&self) -> DatabaseId {
self.database_id
}
pub fn name(&self) -> &'a str {
self.name
}
pub fn qualified(&self) -> QualifiedCollection {
QualifiedCollection::new(self.database_id, self.name)
}
pub fn vshard(&self) -> super::VShardId {
super::VShardId::from_collection(*self)
}
}
#[cfg(test)]
mod tests {
use super::*;
const DB: DatabaseId = DatabaseId::new(1024);
#[test]
fn qualified_input_yields_the_bare_key() {
let qualified = QualifiedCollection::new(DB, "users");
let from_qualified = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
assert_eq!(from_qualified.name(), "users");
assert_eq!(from_qualified, CollectionKey::from_bare(DB, "users"));
assert_eq!(
from_qualified.vshard(),
CollectionKey::from_bare(DB, "users").vshard()
);
}
#[test]
fn qualified_string_cannot_become_a_key_without_dequalifying() {
let qualified = QualifiedCollection::new(DB, "users");
let key = CollectionKey::from_qualified_str(DB, qualified.as_str()).expect("qualified");
assert_ne!(key.name(), qualified.as_str());
assert_eq!(key.name(), "users");
assert_eq!(key.qualified(), qualified);
}
#[test]
fn default_database_names_are_already_bare() {
let key = CollectionKey::from_qualified_str(DatabaseId::DEFAULT, "users").expect("default");
assert_eq!(key, CollectionKey::from_bare(DatabaseId::DEFAULT, "users"));
}
#[test]
fn empty_name_is_the_no_collection_sentinel_in_every_database() {
let key = CollectionKey::from_qualified_str(DB, "").expect("empty");
assert_eq!(key, CollectionKey::from_bare(DB, ""));
}
#[test]
fn exactly_one_qualifier_is_stripped() {
let qualified = QualifiedCollection::new(DB, "1024/nested");
let key = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
assert_eq!(key.name(), "1024/nested");
}
#[test]
fn unqualified_name_in_a_named_database_is_rejected() {
let err = CollectionKey::from_qualified_str(DB, "users").expect_err("unqualified");
assert_eq!(
err,
CollectionKeyError::NotQualified {
database_id: 1024,
name: "users".to_owned(),
}
);
}
#[test]
fn foreign_database_qualifier_is_rejected() {
assert!(CollectionKey::from_qualified_str(DB, "7/users").is_err());
assert!(CollectionKey::from_qualified_str(DB, "+1024/users").is_err());
assert!(CollectionKey::from_qualified_str(DB, "01024/users").is_err());
assert!(CollectionKey::from_qualified_str(DB, "/users").is_err());
}
#[test]
fn bare_and_qualified_hashes_differ_for_a_named_database() {
let mut differs = false;
for i in 0..64 {
let name = format!("coll_{i}");
let qualified = QualifiedCollection::new(DB, &name);
let key = CollectionKey::from_qualified(DB, &qualified).expect("qualified");
let raw_qualified = CollectionKey::from_bare(DB, qualified.as_str());
assert_eq!(key.vshard(), CollectionKey::from_bare(DB, &name).vshard());
differs |= key.vshard() != raw_qualified.vshard();
}
assert!(differs);
}
}