use crate::persona::claim_types::Registry;
const EXTENSION_PREFIX: &str = "x:";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Family {
Identity,
Contact,
Public,
Gated,
Declared,
Unregistered,
}
impl Family {
#[must_use]
pub fn all() -> [Family; 6] {
[
Family::Identity,
Family::Contact,
Family::Public,
Family::Gated,
Family::Declared,
Family::Unregistered,
]
}
#[must_use]
pub fn placed_roots() -> [&'static str; 10] {
[
"name", "person", "email", "phone", "address", "account", "url", "org", "payment",
"gov",
]
}
#[must_use]
pub fn of(claim_type: &str, registry: &Registry) -> Family {
if claim_type.starts_with(EXTENSION_PREFIX) {
return Family::Unregistered;
}
let root = claim_type.split('.').next().unwrap_or_default();
if !registry.registered_roots().contains(root) {
return Family::Unregistered;
}
match root {
"name" | "person" => Family::Identity,
"email" | "phone" | "address" => Family::Contact,
"account" | "url" | "org" => Family::Public,
"payment" | "gov" => Family::Gated,
_ => Family::Declared,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Family::Identity => "Who you are",
Family::Contact => "How to reach you",
Family::Public => "Where you already appear",
Family::Gated => "Your agent asks first",
Family::Declared => "Your agent's own",
Family::Unregistered => "Not in the registry",
}
}
#[must_use]
pub fn note(self) -> &'static str {
match self {
Family::Identity => "names, and what is true of you as a person",
Family::Contact => "an address someone can arrive at",
Family::Public => "handles, pages and roles others can already see",
Family::Gated => {
"the registry gates these — your agent asks you again before one of them leaves"
}
Family::Declared => {
"declared by this agent rather than by the shared registry — it decides how these \
are treated"
}
Family::Unregistered => {
"your agent's claim-type table does not declare these, so they are treated as the \
most private kind"
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn spec_0_1() -> Registry {
Registry::vendored()
}
#[test]
fn a_registered_root_lands_in_its_group() {
let r = spec_0_1();
assert_eq!(Family::of("name.legal", &r), Family::Identity);
assert_eq!(Family::of("person.birthDate", &r), Family::Identity);
assert_eq!(Family::of("email.work", &r), Family::Contact);
assert_eq!(Family::of("phone.mobile", &r), Family::Contact);
assert_eq!(Family::of("address.postal", &r), Family::Contact);
assert_eq!(Family::of("account.handle", &r), Family::Public);
assert_eq!(Family::of("org.role", &r), Family::Public);
assert_eq!(Family::of("url.homepage", &r), Family::Public);
assert_eq!(Family::of("payment.card", &r), Family::Gated);
assert_eq!(Family::of("gov.id.passport", &r), Family::Gated);
}
#[test]
fn a_new_token_groups_with_its_declared_root() {
assert_eq!(Family::of("payment.giftCard", &spec_0_1()), Family::Gated);
}
#[test]
fn an_undeclared_root_is_never_guessed_at() {
let r = spec_0_1();
assert_eq!(Family::of("profile.github", &r), Family::Unregistered);
assert_eq!(Family::of("employer", &r), Family::Unregistered);
assert_eq!(Family::of("medical.condition", &r), Family::Unregistered);
}
#[test]
fn an_extension_token_never_borrows_a_group() {
let r = spec_0_1();
assert_eq!(Family::of("x:name.legal", &r), Family::Unregistered);
assert_eq!(Family::of("x:payment.card", &r), Family::Unregistered);
}
#[test]
fn a_root_only_the_agent_declares_is_its_own() {
let served = Registry::from_wire(&serde_json::json!({
"registryVersion": "0.1",
"entries": [
{ "type": "name", "sensitivity": "normal", "release": "consent", "mask": "none" },
{ "type": "profile", "sensitivity": "normal", "release": "consent", "mask": "none" },
{ "type": "employer", "sensitivity": "normal", "release": "consent", "mask": "none" }
],
"unregistered": { "sensitivity": "high", "release": "consent", "mask": "full" },
"strictness": {
"sensitivity": ["high", "normal"],
"release": ["stepUp", "consent"],
"mask": ["full", "last2", "last4", "emailLocal", "none"]
}
}));
assert_eq!(Family::of("profile.github", &served), Family::Declared);
assert_eq!(Family::of("employer", &served), Family::Declared);
assert_eq!(
Family::of("medical.condition", &served),
Family::Unregistered
);
assert_eq!(Family::of("name.given", &served), Family::Identity);
}
#[test]
fn every_placed_root_is_actually_placed() {
let r = spec_0_1();
for root in Family::placed_roots() {
let family = Family::of(root, &r);
assert!(
!matches!(family, Family::Declared | Family::Unregistered),
"{root} claims to be placed but resolves to {family:?}"
);
}
}
#[test]
fn every_family_has_its_own_words() {
let mut labels: Vec<&str> = Family::all().iter().map(|f| f.label()).collect();
labels.sort_unstable();
let count = labels.len();
labels.dedup();
assert_eq!(labels.len(), count, "two families share a heading");
for family in Family::all() {
assert!(!family.note().is_empty(), "{family:?} has no line");
}
}
}