use serde_json::{Map, Value}; fn normalize_token(input: &str) -> String { input.trim().to_lowercase().replace('_', "-") } fn normalize_subtype_token(input: &str) -> String { normalize_token(input) } fn map_legacy_entry_type(input: &str) -> Option<(&'static str, &'static str)> { match input { "log-ingestion-endpoint" => Some(("service", "log-ingestion")), "cloud-api" => Some(("service", "cloud-api")), "git-server" => Some(("service", "git")), "mqtt-broker" => Some(("service", "mqtt-broker")), "database" => Some(("service", "database")), "monitoring-dashboard" => Some(("service", "monitoring")), "dns-api" => Some(("service", "dns-api")), "notification-webhook" => Some(("service", "webhook")), "api-endpoint" => Some(("service", "api-endpoint")), "credential" | "credential-key" => Some(("service", "credential")), "key" => Some(("service", "credential")), _ => None, } } /// Normalize entry `type` and optionally backfill `metadata.subtype` for legacy values. /// /// This keeps backward compatibility: /// - stable primary types stay unchanged /// - known legacy long-tail types are mapped to `service` + `metadata.subtype` /// - unknown values are kept (normalized to kebab-case) instead of hard failing pub fn normalize_entry_type_and_metadata( entry_type: &str, metadata: &mut Map, ) -> String { let original_raw = entry_type.trim(); let normalized = normalize_token(original_raw); if normalized.is_empty() { return String::new(); } if let Some((mapped_type, mapped_subtype)) = map_legacy_entry_type(&normalized) { if !metadata.contains_key("subtype") { metadata.insert( "subtype".to_string(), Value::String(mapped_subtype.to_string()), ); } if !metadata.contains_key("_original_type") && original_raw != mapped_type { metadata.insert( "_original_type".to_string(), Value::String(original_raw.to_string()), ); } return mapped_type.to_string(); } if let Some(subtype) = metadata.get_mut("subtype") && let Some(s) = subtype.as_str() { *subtype = Value::String(normalize_subtype_token(s)); } normalized } /// Canonical secret type options for UI dropdowns. pub const SECRET_TYPE_OPTIONS: &[&str] = &[ "text", "password", "token", "api-key", "ssh-key", "url", "phone", "id-card", ]; #[cfg(test)] mod tests { use super::*; use serde_json::{Map, Value}; #[test] fn normalize_entry_type_maps_legacy_type_and_backfills_metadata() { let mut metadata = Map::new(); let normalized = normalize_entry_type_and_metadata("git-server", &mut metadata); assert_eq!(normalized, "service"); assert_eq!( metadata.get("subtype"), Some(&Value::String("git".to_string())) ); assert_eq!( metadata.get("_original_type"), Some(&Value::String("git-server".to_string())) ); } #[test] fn normalize_entry_type_normalizes_existing_subtype() { let mut metadata = Map::new(); metadata.insert( "subtype".to_string(), Value::String("Cloud_API".to_string()), ); let normalized = normalize_entry_type_and_metadata("service", &mut metadata); assert_eq!(normalized, "service"); assert_eq!( metadata.get("subtype"), Some(&Value::String("cloud-api".to_string())) ); } }