use std::fmt;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::filter_ir::{AuthScope, FilterIR, FilterBuilder};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Namespace(String);
impl Namespace {
pub const MAX_LENGTH: usize = 256;
pub fn new(name: impl Into<String>) -> Result<Self, NamespaceError> {
let name = name.into();
Self::validate(&name)?;
Ok(Self(name))
}
#[allow(dead_code)]
pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
Self(name.into())
}
fn validate(name: &str) -> Result<(), NamespaceError> {
Self::validate_name(name)
}
pub fn validate_name(name: &str) -> Result<(), NamespaceError> {
if name.is_empty() {
return Err(NamespaceError::Empty);
}
if name.len() > Self::MAX_LENGTH {
return Err(NamespaceError::TooLong {
length: name.len(),
max: Self::MAX_LENGTH,
});
}
let first = name.chars().next().unwrap();
if first == '.' || first == '-' {
return Err(NamespaceError::InvalidStart(first));
}
for (i, ch) in name.chars().enumerate() {
if !ch.is_alphanumeric() && ch != '_' && ch != '-' && ch != '.' {
return Err(NamespaceError::InvalidChar { ch, position: i });
}
}
Ok(())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for Namespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Namespace {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum NamespaceError {
#[error("namespace cannot be empty")]
Empty,
#[error("namespace too long: {length} > {max}")]
TooLong { length: usize, max: usize },
#[error("namespace cannot start with '{0}'")]
InvalidStart(char),
#[error("invalid character '{ch}' at position {position}")]
InvalidChar { ch: char, position: usize },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum NamespaceScope {
Single(Namespace),
Multiple(Vec<Namespace>),
}
impl NamespaceScope {
pub fn single(ns: Namespace) -> Self {
Self::Single(ns)
}
pub fn multiple(namespaces: Vec<Namespace>) -> Result<Self, NamespaceError> {
if namespaces.is_empty() {
return Err(NamespaceError::Empty);
}
Ok(Self::Multiple(namespaces))
}
pub fn namespaces(&self) -> Vec<&Namespace> {
match self {
Self::Single(ns) => vec![ns],
Self::Multiple(nss) => nss.iter().collect(),
}
}
pub fn contains(&self, ns: &Namespace) -> bool {
match self {
Self::Single(single) => single == ns,
Self::Multiple(multiple) => multiple.contains(ns),
}
}
pub fn validate_against(&self, auth: &AuthScope) -> Result<(), ScopeError> {
for ns in self.namespaces() {
if !auth.is_namespace_allowed(ns.as_str()) {
return Err(ScopeError::NamespaceNotAllowed(ns.clone()));
}
}
Ok(())
}
pub fn to_filter_ir(&self) -> FilterIR {
match self {
Self::Single(ns) => FilterBuilder::new()
.namespace(ns.as_str())
.build(),
Self::Multiple(nss) => {
use crate::filter_ir::{FilterAtom, FilterValue};
FilterIR::from_atom(FilterAtom::in_set(
"namespace",
nss.iter()
.map(|ns| FilterValue::String(ns.as_str().to_string()))
.collect(),
))
}
}
}
}
impl fmt::Display for NamespaceScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Single(ns) => write!(f, "{}", ns),
Self::Multiple(nss) => {
let names: Vec<_> = nss.iter().map(|ns| ns.as_str()).collect();
write!(f, "[{}]", names.join(", "))
}
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ScopeError {
#[error("namespace not allowed: {0}")]
NamespaceNotAllowed(Namespace),
#[error("auth scope expired")]
AuthExpired,
#[error("insufficient capabilities for this operation")]
InsufficientCapabilities,
}
#[derive(Debug, Clone)]
pub struct ScopedQuery<Q> {
scope: NamespaceScope,
query: Q,
filters: FilterIR,
}
impl<Q> ScopedQuery<Q> {
pub fn new(scope: NamespaceScope, query: Q) -> Self {
Self {
scope,
query,
filters: FilterIR::all(),
}
}
pub fn in_namespace(namespace: Namespace, query: Q) -> Self {
Self::new(NamespaceScope::Single(namespace), query)
}
pub fn with_filters(mut self, filters: FilterIR) -> Self {
self.filters = filters;
self
}
pub fn scope(&self) -> &NamespaceScope {
&self.scope
}
pub fn query(&self) -> &Q {
&self.query
}
pub fn filters(&self) -> &FilterIR {
&self.filters
}
pub fn effective_filter(&self) -> FilterIR {
self.scope.to_filter_ir().and(self.filters.clone())
}
pub fn validate(&self, auth: &AuthScope) -> Result<(), ScopeError> {
if auth.is_expired() {
return Err(ScopeError::AuthExpired);
}
self.scope.validate_against(auth)?;
Ok(())
}
pub fn into_query(self) -> Q {
self.query
}
}
#[derive(Debug, Clone)]
pub struct QueryRequest<Q> {
query: ScopedQuery<Q>,
auth: Arc<AuthScope>,
}
impl<Q> QueryRequest<Q> {
pub fn new(query: ScopedQuery<Q>, auth: Arc<AuthScope>) -> Result<Self, ScopeError> {
query.validate(&auth)?;
Ok(Self { query, auth })
}
pub fn query(&self) -> &ScopedQuery<Q> {
&self.query
}
pub fn auth(&self) -> &AuthScope {
&self.auth
}
pub fn effective_filter(&self) -> FilterIR {
self.auth.to_filter_ir()
.and(self.query.effective_filter())
}
pub fn namespace_scope(&self) -> &NamespaceScope {
self.query.scope()
}
}
pub fn ns(name: &str) -> Result<Namespace, NamespaceError> {
Namespace::new(name)
}
pub fn scope(name: &str) -> Result<NamespaceScope, NamespaceError> {
Ok(NamespaceScope::Single(Namespace::new(name)?))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DatabaseId {
pub namespace: String,
pub name: String,
}
impl DatabaseId {
pub const MAX_LENGTH: usize = 256;
pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Result<Self, NamespaceError> {
let namespace = namespace.into();
let name = name.into();
Namespace::validate_name(&name)?;
Ok(Self { namespace, name })
}
pub fn qualified_name(&self) -> String {
format!("{}/{}", self.namespace, self.name)
}
}
impl fmt::Display for DatabaseId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.namespace, self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualifiedTable {
pub namespace: String,
pub database: String,
pub table: String,
}
impl QualifiedTable {
pub fn new(
namespace: impl Into<String>,
database: impl Into<String>,
table: impl Into<String>,
) -> Self {
Self {
namespace: namespace.into(),
database: database.into(),
table: table.into(),
}
}
pub fn qualified_name(&self) -> String {
format!("{}/{}/{}", self.namespace, self.database, self.table)
}
pub fn storage_prefix(&self) -> String {
format!("{}:{}:{}", self.namespace, self.database, self.table)
}
}
impl fmt::Display for QualifiedTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}/{}", self.namespace, self.database, self.table)
}
}
#[derive(Debug, Clone, Default)]
pub struct NamespaceRegistry {
databases: std::collections::HashMap<String, std::collections::HashSet<String>>,
tables: std::collections::HashMap<(String, String), std::collections::HashSet<String>>,
}
impl NamespaceRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn create_namespace(&mut self, namespace: &str) -> Result<(), NamespaceError> {
Namespace::validate_name(namespace)?;
self.databases.entry(namespace.to_string()).or_default();
Ok(())
}
pub fn create_database(&mut self, namespace: &str, database: &str) -> Result<(), NamespaceError> {
Namespace::validate_name(database)?;
let dbs = self.databases.entry(namespace.to_string()).or_default();
dbs.insert(database.to_string());
self.tables.entry((namespace.to_string(), database.to_string())).or_default();
Ok(())
}
pub fn create_table(&mut self, namespace: &str, database: &str, table: &str) -> Result<(), NamespaceError> {
Namespace::validate_name(table)?;
let dbs = self.databases.entry(namespace.to_string()).or_default();
dbs.insert(database.to_string());
let tables = self.tables.entry((namespace.to_string(), database.to_string())).or_default();
tables.insert(table.to_string());
Ok(())
}
pub fn list_databases(&self, namespace: &str) -> Vec<&str> {
self.databases.get(namespace)
.map(|dbs| dbs.iter().map(|s| s.as_str()).collect())
.unwrap_or_default()
}
pub fn list_tables(&self, namespace: &str, database: &str) -> Vec<&str> {
self.tables.get(&(namespace.to_string(), database.to_string()))
.map(|tables| tables.iter().map(|s| s.as_str()).collect())
.unwrap_or_default()
}
pub fn namespace_exists(&self, namespace: &str) -> bool {
self.databases.contains_key(namespace)
}
pub fn database_exists(&self, namespace: &str, database: &str) -> bool {
self.databases.get(namespace)
.map(|dbs| dbs.contains(database))
.unwrap_or(false)
}
pub fn table_exists(&self, namespace: &str, database: &str, table: &str) -> bool {
self.tables.get(&(namespace.to_string(), database.to_string()))
.map(|tables| tables.contains(table))
.unwrap_or(false)
}
pub fn drop_database(&mut self, namespace: &str, database: &str) -> bool {
self.tables.remove(&(namespace.to_string(), database.to_string()));
self.databases.get_mut(namespace)
.map(|dbs| dbs.remove(database))
.unwrap_or(false)
}
pub fn drop_table(&mut self, namespace: &str, database: &str, table: &str) -> bool {
self.tables.get_mut(&(namespace.to_string(), database.to_string()))
.map(|tables| tables.remove(table))
.unwrap_or(false)
}
pub fn drop_namespace(&mut self, namespace: &str) -> bool {
if !self.databases.contains_key(namespace) {
return false;
}
let db_names: Vec<String> = self.databases.get(namespace)
.map(|dbs| dbs.iter().cloned().collect())
.unwrap_or_default();
for db in &db_names {
self.tables.remove(&(namespace.to_string(), db.clone()));
}
self.databases.remove(namespace);
true
}
pub fn resolve_table(&self, qualified: &QualifiedTable) -> bool {
self.table_exists(&qualified.namespace, &qualified.database, &qualified.table)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_namespace_validation() {
assert!(Namespace::new("production").is_ok());
assert!(Namespace::new("my_namespace").is_ok());
assert!(Namespace::new("project-123").is_ok());
assert!(Namespace::new("v1.0.0").is_ok());
assert!(Namespace::new("").is_err()); assert!(Namespace::new("-starts-with-dash").is_err());
assert!(Namespace::new(".starts-with-dot").is_err());
assert!(Namespace::new("has spaces").is_err());
assert!(Namespace::new("has@symbol").is_err());
}
#[test]
fn test_namespace_scope_single() {
let ns = Namespace::new("production").unwrap();
let scope = NamespaceScope::single(ns.clone());
assert!(scope.contains(&ns));
assert!(!scope.contains(&Namespace::new("staging").unwrap()));
}
#[test]
fn test_namespace_scope_multiple() {
let ns1 = Namespace::new("prod").unwrap();
let ns2 = Namespace::new("staging").unwrap();
let scope = NamespaceScope::multiple(vec![ns1.clone(), ns2.clone()]).unwrap();
assert!(scope.contains(&ns1));
assert!(scope.contains(&ns2));
assert!(!scope.contains(&Namespace::new("dev").unwrap()));
}
#[test]
fn test_scope_to_filter_ir() {
let scope = NamespaceScope::single(Namespace::new("production").unwrap());
let filter = scope.to_filter_ir();
assert!(filter.constrains_field("namespace"));
assert_eq!(filter.clauses.len(), 1);
}
#[test]
fn test_scoped_query_effective_filter() {
let ns = Namespace::new("production").unwrap();
let user_filter = FilterBuilder::new()
.eq("source", "documents")
.build();
let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
.with_filters(user_filter);
let effective = query.effective_filter();
assert!(effective.constrains_field("namespace"));
assert!(effective.constrains_field("source"));
}
#[test]
fn test_query_request_validation() {
let ns = Namespace::new("production").unwrap();
let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ());
let auth = Arc::new(AuthScope::for_namespace("production"));
assert!(QueryRequest::new(query.clone(), auth).is_ok());
let auth2 = Arc::new(AuthScope::for_namespace("staging"));
assert!(QueryRequest::new(query, auth2).is_err());
}
#[test]
fn test_query_request_effective_filter() {
let ns = Namespace::new("production").unwrap();
let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
.with_filters(FilterBuilder::new().eq("type", "article").build());
let auth = Arc::new(
AuthScope::for_namespace("production")
.with_tenant("acme")
);
let request = QueryRequest::new(query, auth).unwrap();
let effective = request.effective_filter();
assert!(effective.constrains_field("namespace"));
assert!(effective.constrains_field("tenant_id"));
assert!(effective.constrains_field("type"));
}
#[test]
fn test_database_id_creation() {
let db = DatabaseId::new("production", "app").unwrap();
assert_eq!(db.namespace, "production");
assert_eq!(db.name, "app");
assert_eq!(db.qualified_name(), "production/app");
}
#[test]
fn test_qualified_table() {
let qt = QualifiedTable::new("production", "app", "users");
assert_eq!(qt.qualified_name(), "production/app/users");
assert_eq!(qt.storage_prefix(), "production:app:users");
}
#[test]
fn test_namespace_registry_basic() {
let mut reg = NamespaceRegistry::new();
reg.create_namespace("prod").unwrap();
assert!(reg.namespace_exists("prod"));
assert!(!reg.namespace_exists("staging"));
}
#[test]
fn test_namespace_registry_databases() {
let mut reg = NamespaceRegistry::new();
reg.create_namespace("prod").unwrap();
reg.create_database("prod", "app").unwrap();
reg.create_database("prod", "analytics").unwrap();
assert!(reg.database_exists("prod", "app"));
assert!(reg.database_exists("prod", "analytics"));
assert!(!reg.database_exists("prod", "logs"));
let dbs = reg.list_databases("prod");
assert_eq!(dbs.len(), 2);
}
#[test]
fn test_namespace_registry_tables() {
let mut reg = NamespaceRegistry::new();
reg.create_table("prod", "app", "users").unwrap();
reg.create_table("prod", "app", "posts").unwrap();
assert!(reg.table_exists("prod", "app", "users"));
assert!(reg.table_exists("prod", "app", "posts"));
assert!(!reg.table_exists("prod", "app", "comments"));
assert!(reg.database_exists("prod", "app"));
}
#[test]
fn test_namespace_registry_drop() {
let mut reg = NamespaceRegistry::new();
reg.create_table("prod", "app", "users").unwrap();
reg.create_table("prod", "app", "posts").unwrap();
reg.create_table("prod", "analytics", "events").unwrap();
assert!(reg.drop_table("prod", "app", "users"));
assert!(!reg.table_exists("prod", "app", "users"));
assert!(reg.table_exists("prod", "app", "posts"));
assert!(reg.drop_database("prod", "app"));
assert!(!reg.database_exists("prod", "app"));
assert!(!reg.table_exists("prod", "app", "posts"));
assert!(reg.table_exists("prod", "analytics", "events"));
assert!(reg.drop_namespace("prod"));
assert!(!reg.namespace_exists("prod"));
assert!(!reg.table_exists("prod", "analytics", "events"));
}
#[test]
fn test_qualified_table_resolve() {
let mut reg = NamespaceRegistry::new();
reg.create_table("prod", "app", "users").unwrap();
let qt = QualifiedTable::new("prod", "app", "users");
assert!(reg.resolve_table(&qt));
let missing = QualifiedTable::new("prod", "app", "absent");
assert!(!reg.resolve_table(&missing));
}
}