use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
fn registry() -> &'static RwLock<HashMap<String, Vec<(&'static str, &'static str)>>> {
static REGISTRY: OnceLock<RwLock<HashMap<String, Vec<(&'static str, &'static str)>>>> =
OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
}
fn key(table: &str) -> String {
table
.rsplit('.')
.next()
.unwrap_or(table)
.trim_matches('"')
.to_ascii_lowercase()
}
pub fn register(table: &str, fields: &'static [(&'static str, &'static str)]) {
if fields.is_empty() {
return;
}
let mut map = registry().write().unwrap_or_else(|e| e.into_inner());
let entry = map.entry(key(table)).or_default();
for field in fields {
if !entry.iter().any(|(f, _)| *f == field.0) {
entry.push(*field);
}
}
}
pub fn field_permissions_of(table: &str) -> Vec<(&'static str, &'static str)> {
let map = registry().read().unwrap_or_else(|e| e.into_inner());
map.get(&key(table)).cloned().unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_table_registered_in_any_spelling_answers_its_gated_fields() {
register(
"registry_probe.payroll",
&[("baseSalary", "payroll_entry:read_salary")],
);
register("PAYROLL_OTHER", &[]);
assert_eq!(
field_permissions_of("payroll"),
vec![("baseSalary", "payroll_entry:read_salary")]
);
assert_eq!(
field_permissions_of("\"hr\".\"payroll\""),
vec![("baseSalary", "payroll_entry:read_salary")]
);
assert!(field_permissions_of("payroll_other").is_empty());
assert!(field_permissions_of("never_registered").is_empty());
}
#[test]
fn a_field_registered_twice_answers_once() {
register(
"registry_dupe.accounts",
&[("bidValue", "account:read_bid")],
);
register("other.accounts", &[("bidValue", "account:read_bid")]);
assert_eq!(
field_permissions_of("accounts"),
vec![("bidValue", "account:read_bid")]
);
}
}