use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use anyhow::{anyhow, Result};
#[derive(Debug, Clone)]
pub struct TenantSchema {
pub tenant_id: String,
pub sdl: String,
pub version: u64,
}
impl TenantSchema {
fn new(tenant_id: impl Into<String>, sdl: impl Into<String>, version: u64) -> Self {
Self {
tenant_id: tenant_id.into(),
sdl: sdl.into(),
version,
}
}
}
pub struct TenantSchemaRegistry {
schemas: Arc<RwLock<HashMap<String, TenantSchema>>>,
versions: Arc<RwLock<HashMap<String, u64>>>,
}
impl std::fmt::Debug for TenantSchemaRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let count = self.schemas.read().map(|s| s.len()).unwrap_or(0);
f.debug_struct("TenantSchemaRegistry")
.field("tenant_count", &count)
.finish()
}
}
impl TenantSchemaRegistry {
pub fn new() -> Self {
Self {
schemas: Arc::new(RwLock::new(HashMap::new())),
versions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register_schema(&self, tenant_id: &str, sdl: &str) -> Result<()> {
let mut versions = self
.versions
.write()
.map_err(|_| anyhow!("TenantSchemaRegistry versions lock poisoned"))?;
let version = versions
.entry(tenant_id.to_string())
.and_modify(|v| *v += 1)
.or_insert(1);
let schema = TenantSchema::new(tenant_id, sdl, *version);
self.schemas
.write()
.map_err(|_| anyhow!("TenantSchemaRegistry schemas lock poisoned"))?
.insert(tenant_id.to_string(), schema);
Ok(())
}
pub fn get_schema(&self, tenant_id: &str) -> Option<TenantSchema> {
self.schemas.read().ok()?.get(tenant_id).cloned()
}
pub fn list_tenants(&self) -> Vec<String> {
self.schemas
.read()
.map(|s| s.keys().cloned().collect())
.unwrap_or_default()
}
pub fn remove_schema(&self, tenant_id: &str) -> bool {
self.schemas
.write()
.map(|mut s| s.remove(tenant_id).is_some())
.unwrap_or(false)
}
pub fn tenant_count(&self) -> usize {
self.schemas.read().map(|s| s.len()).unwrap_or(0)
}
}
impl Default for TenantSchemaRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_sdl() -> &'static str {
r#"type Query { hello: String }"#
}
#[test]
fn test_register_and_get_schema() {
let reg = TenantSchemaRegistry::new();
reg.register_schema("acme", sample_sdl())
.expect("register ok");
let schema = reg.get_schema("acme").expect("should exist");
assert_eq!(schema.tenant_id, "acme");
assert_eq!(schema.sdl, sample_sdl());
assert_eq!(schema.version, 1);
}
#[test]
fn test_get_missing_schema_returns_none() {
let reg = TenantSchemaRegistry::new();
assert!(reg.get_schema("ghost").is_none());
}
#[test]
fn test_register_increments_version() {
let reg = TenantSchemaRegistry::new();
reg.register_schema("t1", "type Query { v1: String }")
.expect("should succeed");
reg.register_schema("t1", "type Query { v2: String }")
.expect("should succeed");
let schema = reg.get_schema("t1").expect("should succeed");
assert_eq!(schema.version, 2);
assert!(schema.sdl.contains("v2"));
}
#[test]
fn test_list_tenants() {
let reg = TenantSchemaRegistry::new();
reg.register_schema("a", sample_sdl())
.expect("should succeed");
reg.register_schema("b", sample_sdl())
.expect("should succeed");
let mut tenants = reg.list_tenants();
tenants.sort();
assert_eq!(tenants, vec!["a", "b"]);
}
#[test]
fn test_remove_schema() {
let reg = TenantSchemaRegistry::new();
reg.register_schema("gone", sample_sdl())
.expect("should succeed");
assert!(reg.remove_schema("gone"));
assert!(reg.get_schema("gone").is_none());
assert!(!reg.remove_schema("gone")); }
#[test]
fn test_tenant_count() {
let reg = TenantSchemaRegistry::new();
assert_eq!(reg.tenant_count(), 0);
reg.register_schema("t1", sample_sdl())
.expect("should succeed");
assert_eq!(reg.tenant_count(), 1);
reg.register_schema("t2", sample_sdl())
.expect("should succeed");
assert_eq!(reg.tenant_count(), 2);
reg.remove_schema("t1");
assert_eq!(reg.tenant_count(), 1);
}
#[test]
fn test_multiple_tenants_isolated() {
let reg = TenantSchemaRegistry::new();
reg.register_schema("tenant_a", "type Query { a: String }")
.expect("should succeed");
reg.register_schema("tenant_b", "type Query { b: String }")
.expect("should succeed");
let a = reg.get_schema("tenant_a").expect("should succeed");
let b = reg.get_schema("tenant_b").expect("should succeed");
assert!(a.sdl.contains("a: String"));
assert!(b.sdl.contains("b: String"));
assert_ne!(a.sdl, b.sdl);
}
}