use parking_lot::RwLock;
pub const DEFAULT_TABLE: &str = "sync_items";
#[derive(Debug)]
pub struct SchemaRegistry {
mappings: RwLock<Vec<(String, String)>>,
bypass: bool,
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self::new()
}
}
impl SchemaRegistry {
#[must_use]
pub fn new() -> Self {
Self {
mappings: RwLock::new(Vec::new()),
bypass: false,
}
}
#[must_use]
pub fn bypass() -> Self {
Self {
mappings: RwLock::new(Vec::new()),
bypass: true,
}
}
pub fn register(&self, prefix: &str, table_name: &str) {
let mut mappings = self.mappings.write();
if let Some(pos) = mappings.iter().position(|(p, _)| p == prefix) {
mappings[pos].1 = table_name.to_string();
} else {
mappings.push((prefix.to_string(), table_name.to_string()));
}
mappings.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
}
pub fn unregister(&self, prefix: &str) -> bool {
let mut mappings = self.mappings.write();
if let Some(pos) = mappings.iter().position(|(p, _)| p == prefix) {
mappings.remove(pos);
true
} else {
false
}
}
#[must_use]
pub fn table_for_key(&self, key: &str) -> &'static str {
if self.bypass {
return DEFAULT_TABLE;
}
let mappings = self.mappings.read();
for (prefix, table) in mappings.iter() {
if key.starts_with(prefix) {
return Box::leak(table.clone().into_boxed_str());
}
}
DEFAULT_TABLE
}
#[must_use]
pub fn prefixes_for_table(&self, table_name: &str) -> Vec<String> {
self.mappings
.read()
.iter()
.filter(|(_, t)| t == table_name)
.map(|(p, _)| p.clone())
.collect()
}
#[must_use]
pub fn tables(&self) -> Vec<String> {
let mappings = self.mappings.read();
let mut tables: Vec<String> = mappings.iter().map(|(_, t)| t.clone()).collect();
tables.sort();
tables.dedup();
tables
}
#[must_use]
pub fn len(&self) -> usize {
self.mappings.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.mappings.read().is_empty()
}
#[must_use]
pub fn is_bypass(&self) -> bool {
self.bypass
}
pub fn clear(&self) {
self.mappings.write().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_registry_returns_default() {
let registry = SchemaRegistry::new();
assert_eq!(registry.table_for_key("any:key"), DEFAULT_TABLE);
assert_eq!(registry.table_for_key("view:users:alice"), DEFAULT_TABLE);
}
#[test]
fn test_basic_routing() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
assert_eq!(registry.table_for_key("view:users:alice"), "users_items");
assert_eq!(registry.table_for_key("view:users:bob"), "users_items");
assert_eq!(registry.table_for_key("view:orders:123"), DEFAULT_TABLE);
}
#[test]
fn test_multiple_prefixes_same_table() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
registry.register("crdt:users:", "users_items");
assert_eq!(registry.table_for_key("view:users:alice"), "users_items");
assert_eq!(registry.table_for_key("crdt:users:bob"), "users_items");
}
#[test]
fn test_longest_prefix_wins() {
let registry = SchemaRegistry::new();
registry.register("crdt:", "crdt_items");
registry.register("crdt:users:", "users_items");
assert_eq!(registry.table_for_key("crdt:users:alice"), "users_items");
assert_eq!(registry.table_for_key("crdt:orders:123"), "crdt_items");
}
#[test]
fn test_bypass_mode() {
let registry = SchemaRegistry::bypass();
registry.register("view:users:", "users_items");
assert_eq!(registry.table_for_key("view:users:alice"), DEFAULT_TABLE);
assert!(registry.is_bypass());
}
#[test]
fn test_unregister() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
assert_eq!(registry.table_for_key("view:users:alice"), "users_items");
assert!(registry.unregister("view:users:"));
assert_eq!(registry.table_for_key("view:users:alice"), DEFAULT_TABLE);
assert!(!registry.unregister("view:users:"));
}
#[test]
fn test_update_existing_prefix() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
assert_eq!(registry.table_for_key("view:users:alice"), "users_items");
registry.register("view:users:", "users_v2_items");
assert_eq!(registry.table_for_key("view:users:alice"), "users_v2_items");
assert_eq!(registry.len(), 1);
}
#[test]
fn test_prefixes_for_table() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
registry.register("crdt:users:", "users_items");
registry.register("view:orders:", "orders_items");
let prefixes = registry.prefixes_for_table("users_items");
assert_eq!(prefixes.len(), 2);
assert!(prefixes.contains(&"view:users:".to_string()));
assert!(prefixes.contains(&"crdt:users:".to_string()));
}
#[test]
fn test_tables() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
registry.register("crdt:users:", "users_items");
registry.register("view:orders:", "orders_items");
let tables = registry.tables();
assert_eq!(tables.len(), 2);
assert!(tables.contains(&"orders_items".to_string()));
assert!(tables.contains(&"users_items".to_string()));
}
#[test]
fn test_clear() {
let registry = SchemaRegistry::new();
registry.register("view:users:", "users_items");
registry.register("view:orders:", "orders_items");
assert_eq!(registry.len(), 2);
registry.clear();
assert!(registry.is_empty());
assert_eq!(registry.table_for_key("view:users:alice"), DEFAULT_TABLE);
}
#[test]
fn test_order_independence() {
let registry = SchemaRegistry::new();
registry.register("crdt:users:", "users_items"); registry.register("crdt:", "crdt_items");
assert_eq!(registry.table_for_key("crdt:users:alice"), "users_items");
assert_eq!(registry.table_for_key("crdt:orders:123"), "crdt_items");
let registry2 = SchemaRegistry::new();
registry2.register("crdt:", "crdt_items"); registry2.register("crdt:users:", "users_items");
assert_eq!(registry2.table_for_key("crdt:users:alice"), "users_items");
assert_eq!(registry2.table_for_key("crdt:orders:123"), "crdt_items");
}
}