use thiserror::Error;
pub const LOCAL_DEV_TENANT_ID: &str = "dag_db-local";
pub const LOCAL_DEV_NAMESPACE: &str = "dag_db";
pub const MAX_TENANT_FIELD_LEN: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TenantIdentityError {
#[error("tenant_identity_empty: {field}")]
Empty {
field: &'static str,
},
#[error("tenant_identity_too_long: {field}")]
TooLong {
field: &'static str,
},
#[error("tenant_identity_invalid_char: {field}")]
InvalidChar {
field: &'static str,
},
}
pub fn normalize_tenant_id(value: &str) -> Result<String, TenantIdentityError> {
normalize_field("tenant_id", value)
}
pub fn normalize_namespace(value: &str) -> Result<String, TenantIdentityError> {
normalize_field("namespace", value)
}
fn normalize_field(field: &'static str, value: &str) -> Result<String, TenantIdentityError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(TenantIdentityError::Empty { field });
}
if trimmed.len() > MAX_TENANT_FIELD_LEN {
return Err(TenantIdentityError::TooLong { field });
}
if !trimmed.bytes().all(is_accepted_byte) {
return Err(TenantIdentityError::InvalidChar { field });
}
Ok(trimmed.to_owned())
}
const fn is_accepted_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b':' | b'.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_canonical_local_dev_identity() {
assert_eq!(
normalize_tenant_id(LOCAL_DEV_TENANT_ID).expect("canonical tenant accepted"),
LOCAL_DEV_TENANT_ID
);
assert_eq!(
normalize_namespace(LOCAL_DEV_NAMESPACE).expect("canonical namespace accepted"),
LOCAL_DEV_NAMESPACE
);
}
#[test]
fn accepts_real_world_identity_shapes() {
for value in [
"tenant-a",
"tenant_benchmark",
"tenant:a",
"00000000-0000-0000-0000-000000000101",
"did:exo:dagdb-mcp-local",
"the-team-local",
] {
assert_eq!(
normalize_tenant_id(value).expect("real-world tenant accepted"),
value
);
}
}
#[test]
fn trims_surrounding_whitespace() {
assert_eq!(
normalize_tenant_id(" dag_db-local ").expect("trimmed tenant accepted"),
"dag_db-local"
);
}
#[test]
fn rejects_empty_and_whitespace_only() {
assert!(matches!(
normalize_tenant_id(""),
Err(TenantIdentityError::Empty { field: "tenant_id" })
));
assert!(matches!(
normalize_namespace(" "),
Err(TenantIdentityError::Empty { field: "namespace" })
));
}
#[test]
fn rejects_charset_violations() {
for bad in [
"dag db", "tenant/a", "tenant%a", "tenant'; DROP", "ten\nant", "tenant\u{00e9}", ] {
assert!(
matches!(
normalize_tenant_id(bad),
Err(TenantIdentityError::InvalidChar { .. })
),
"expected rejection for {bad:?}"
);
}
}
#[test]
fn rejects_over_length() {
let long = "a".repeat(MAX_TENANT_FIELD_LEN + 1);
assert!(matches!(
normalize_tenant_id(&long),
Err(TenantIdentityError::TooLong { field: "tenant_id" })
));
}
}