#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PrivacyLabel {
Secret,
AccountNumber,
Email,
Phone,
Person,
Address,
Date,
Url,
}
impl PrivacyLabel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Secret => "secret",
Self::AccountNumber => "account_number",
Self::Email => "email",
Self::Phone => "phone",
Self::Person => "person",
Self::Address => "address",
Self::Date => "date",
Self::Url => "url",
}
}
pub const ALL: [PrivacyLabel; 8] = [
Self::Secret,
Self::AccountNumber,
Self::Email,
Self::Phone,
Self::Person,
Self::Address,
Self::Date,
Self::Url,
];
pub fn from_kind(kind: &str) -> Option<Self> {
match kind {
"aws_access_key_id" | "github_token" | "anthropic_api_key" | "openai_api_key"
| "jwt" | "pem_private_key" | "env_assignment" | "slack_webhook"
| "stripe_secret_key" | "google_api_key" | "gitlab_pat" | "database_url" | "secret" => {
Some(Self::Secret)
}
"email" | "private_email" => Some(Self::Email),
"internal_ipv4" | "generic_url" | "private_url" | "url" => Some(Self::Url),
"private_phone" | "phone" => Some(Self::Phone),
"private_person" | "person" => Some(Self::Person),
"private_address" | "address" => Some(Self::Address),
"private_date" | "date" => Some(Self::Date),
"private_account_number" | "account_number" => Some(Self::AccountNumber),
_ => None,
}
}
}
impl std::fmt::Display for PrivacyLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn privacy_label_as_str_stable() {
let got: Vec<&'static str> = PrivacyLabel::ALL.iter().map(|l| l.as_str()).collect();
let mut got_sorted = got.clone();
got_sorted.sort_unstable();
let mut expected = vec![
"secret",
"account_number",
"email",
"phone",
"person",
"address",
"date",
"url",
];
expected.sort_unstable();
assert_eq!(got_sorted, expected, "PrivacyLabel 字面量集合漂移");
assert_eq!(got.len(), 8, "必须恰好 8 个 variant");
for l in PrivacyLabel::ALL {
assert_eq!(format!("{l}"), l.as_str());
}
}
#[test]
fn privacy_label_from_kind_all_variants() {
let samples: &[(&str, PrivacyLabel)] = &[
("github_token", PrivacyLabel::Secret),
("private_account_number", PrivacyLabel::AccountNumber),
("email", PrivacyLabel::Email),
("private_phone", PrivacyLabel::Phone),
("private_person", PrivacyLabel::Person),
("private_address", PrivacyLabel::Address),
("private_date", PrivacyLabel::Date),
("internal_ipv4", PrivacyLabel::Url),
];
let mut seen: Vec<PrivacyLabel> = samples.iter().map(|(_, l)| *l).collect();
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 8, "样本未覆盖所有 variant");
for (kind, expected) in samples {
assert_eq!(
PrivacyLabel::from_kind(kind),
Some(*expected),
"kind {kind:?} 期望映射到 {expected:?}"
);
}
}
#[test]
fn privacy_label_from_kind_unknown_returns_none() {
assert_eq!(PrivacyLabel::from_kind("not_a_kind"), None);
assert_eq!(PrivacyLabel::from_kind(""), None);
assert_eq!(PrivacyLabel::from_kind("PRIVATE_PERSON"), None); }
#[test]
fn privacy_label_from_kind_accepts_both_model_and_bare() {
assert_eq!(
PrivacyLabel::from_kind("private_email"),
PrivacyLabel::from_kind("email")
);
assert_eq!(
PrivacyLabel::from_kind("private_phone"),
PrivacyLabel::from_kind("phone")
);
}
}