use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TenantKey {
pub tenant_id: Option<String>,
pub name: String,
}
impl TenantKey {
pub fn new(tenant_id: Option<&str>, name: &str) -> Self {
Self {
tenant_id: tenant_id.map(|t| t.to_string()),
name: name.to_string(),
}
}
pub fn tenant_label(&self) -> &str {
self.tenant_id.as_deref().unwrap_or("<untenanted>")
}
}
impl fmt::Display for TenantKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}::{}", self.tenant_label(), self.name)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TenantOwner {
pub package: Option<String>,
}
impl TenantOwner {
pub fn unknown() -> Self {
Self { package: None }
}
pub fn package(package: impl Into<String>) -> Self {
Self {
package: Some(package.into()),
}
}
pub fn may_replace(&self, other: &TenantOwner) -> bool {
match (&self.package, &other.package) {
(Some(a), Some(b)) => a == b,
_ => true,
}
}
pub fn label(&self) -> String {
match &self.package {
Some(p) => format!("package '{}'", p),
None => "an unattributed registration".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TenantScope<'a> {
pub tenant_id: Option<&'a str>,
pub is_admin: bool,
}
impl<'a> TenantScope<'a> {
pub fn of(tenant_id: Option<&'a str>) -> Self {
Self {
tenant_id,
is_admin: false,
}
}
pub fn tenant(tenant_id: &'a str) -> Self {
Self::of(Some(tenant_id))
}
pub fn untenanted() -> Self {
Self::of(None)
}
pub fn admin() -> Self {
Self {
tenant_id: None,
is_admin: true,
}
}
pub fn own_key(&self, name: &str) -> TenantKey {
TenantKey::new(self.tenant_id, name)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TenantResolveMiss {
NotFound,
Ambiguous(Vec<String>),
}
impl fmt::Display for TenantResolveMiss {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TenantResolveMiss::NotFound => write!(f, "not found in this tenant scope"),
TenantResolveMiss::Ambiguous(tenants) => write!(
f,
"name is ambiguous across tenants {:?}; address it with an explicit tenant",
tenants
),
}
}
}
pub fn resolve_tenant_key<V>(
map: &HashMap<TenantKey, V>,
scope: TenantScope<'_>,
name: &str,
) -> Result<TenantKey, TenantResolveMiss> {
if let Some(tenant) = scope.tenant_id {
let key = TenantKey::new(Some(tenant), name);
if map.contains_key(&key) {
return Ok(key);
}
}
let untenanted = TenantKey::new(None, name);
if map.contains_key(&untenanted) {
return Ok(untenanted);
}
if scope.is_admin {
let mut matches: Vec<&TenantKey> = map.keys().filter(|k| k.name == name).collect();
matches.sort();
match matches.len() {
0 => return Err(TenantResolveMiss::NotFound),
1 => return Ok(matches[0].clone()),
_ => {
return Err(TenantResolveMiss::Ambiguous(
matches
.iter()
.map(|k| k.tenant_label().to_string())
.collect(),
))
}
}
}
Err(TenantResolveMiss::NotFound)
}
pub fn visible_keys<'m, V>(
map: &'m HashMap<TenantKey, V>,
scope: TenantScope<'_>,
) -> impl Iterator<Item = &'m TenantKey> {
let tenant = scope.tenant_id.map(|t| t.to_string());
let is_admin = scope.is_admin;
map.keys().filter(move |k| {
is_admin || k.tenant_id.is_none() || k.tenant_id.as_deref() == tenant.as_deref()
})
}
#[cfg(test)]
mod tests {
use super::*;
fn map_with(keys: &[(Option<&str>, &str)]) -> HashMap<TenantKey, u32> {
keys.iter()
.enumerate()
.map(|(i, (t, n))| (TenantKey::new(*t, n), i as u32))
.collect()
}
#[test]
fn own_tenant_wins_over_untenanted() {
let m = map_with(&[(None, "wf"), (Some("a"), "wf")]);
let k = resolve_tenant_key(&m, TenantScope::tenant("a"), "wf").unwrap();
assert_eq!(k.tenant_id.as_deref(), Some("a"));
}
#[test]
fn untenanted_is_the_embedded_fallback() {
let m = map_with(&[(None, "wf")]);
let k = resolve_tenant_key(&m, TenantScope::tenant("a"), "wf").unwrap();
assert_eq!(k.tenant_id, None);
let k = resolve_tenant_key(&m, TenantScope::untenanted(), "wf").unwrap();
assert_eq!(k.tenant_id, None);
}
#[test]
fn non_admin_never_reaches_another_tenant() {
let m = map_with(&[(Some("a"), "wf")]);
assert_eq!(
resolve_tenant_key(&m, TenantScope::tenant("b"), "wf").unwrap_err(),
TenantResolveMiss::NotFound
);
assert_eq!(
resolve_tenant_key(&m, TenantScope::untenanted(), "wf").unwrap_err(),
TenantResolveMiss::NotFound
);
}
#[test]
fn admin_resolves_unique_and_refuses_ambiguous() {
let m = map_with(&[(Some("a"), "wf")]);
assert!(resolve_tenant_key(&m, TenantScope::admin(), "wf").is_ok());
let m = map_with(&[(Some("a"), "wf"), (Some("b"), "wf")]);
match resolve_tenant_key(&m, TenantScope::admin(), "wf") {
Err(TenantResolveMiss::Ambiguous(tenants)) => assert_eq!(tenants.len(), 2),
other => panic!("expected ambiguity, got {:?}", other.map(|k| k.to_string())),
}
}
#[test]
fn visible_keys_hides_other_tenants() {
let m = map_with(&[(None, "a"), (Some("t1"), "b"), (Some("t2"), "c")]);
let mut seen: Vec<String> = visible_keys(&m, TenantScope::tenant("t1"))
.map(|k| k.name.clone())
.collect();
seen.sort();
assert_eq!(seen, vec!["a".to_string(), "b".to_string()]);
let all: Vec<String> = visible_keys(&m, TenantScope::admin())
.map(|k| k.name.clone())
.collect();
assert_eq!(all.len(), 3);
}
#[test]
fn owner_replacement_policy() {
let pkg_a = TenantOwner::package("a");
let pkg_b = TenantOwner::package("b");
let unknown = TenantOwner::unknown();
assert!(pkg_a.may_replace(&pkg_a));
assert!(!pkg_a.may_replace(&pkg_b));
assert!(unknown.may_replace(&pkg_a));
assert!(pkg_a.may_replace(&unknown));
}
}