use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MucosalKind {
ApiRequest,
ApiResponse,
McpInvocation,
ExternalLink,
Iframe,
DatabaseQuery,
CrossService,
SubprocessLaunch,
DependencyImport,
UserInput,
FilesystemPath,
EnvironmentVariable,
ShellArgument,
}
impl MucosalKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ApiRequest => "api-request",
Self::ApiResponse => "api-response",
Self::McpInvocation => "mcp-invocation",
Self::ExternalLink => "external-link",
Self::Iframe => "iframe",
Self::DatabaseQuery => "database-query",
Self::CrossService => "cross-service",
Self::SubprocessLaunch => "subprocess-launch",
Self::DependencyImport => "dependency-import",
Self::UserInput => "user-input",
Self::FilesystemPath => "filesystem-path",
Self::EnvironmentVariable => "environment-variable",
Self::ShellArgument => "shell-argument",
}
}
#[must_use]
pub fn parse_kind(s: &str) -> Option<Self> {
let bare = s.strip_prefix("MucosalKind::").unwrap_or(s);
match bare {
"api-request" | "api_request" | "ApiRequest" => Some(Self::ApiRequest),
"api-response" | "api_response" | "ApiResponse" => Some(Self::ApiResponse),
"mcp-invocation" | "mcp_invocation" | "McpInvocation" => Some(Self::McpInvocation),
"external-link" | "external_link" | "ExternalLink" => Some(Self::ExternalLink),
"iframe" | "Iframe" => Some(Self::Iframe),
"database-query" | "database_query" | "DatabaseQuery" => Some(Self::DatabaseQuery),
"cross-service" | "cross_service" | "CrossService" => Some(Self::CrossService),
"subprocess-launch" | "subprocess_launch" | "SubprocessLaunch" => {
Some(Self::SubprocessLaunch)
}
"dependency-import" | "dependency_import" | "DependencyImport" => {
Some(Self::DependencyImport)
}
"user-input" | "user_input" | "UserInput" => Some(Self::UserInput),
"filesystem-path" | "filesystem_path" | "FilesystemPath" => Some(Self::FilesystemPath),
"environment-variable" | "environment_variable" | "EnvironmentVariable" => {
Some(Self::EnvironmentVariable)
}
"shell-argument" | "shell_argument" | "ShellArgument" => Some(Self::ShellArgument),
_ => None,
}
}
#[must_use]
pub const fn all() -> [Self; 13] {
[
Self::ApiRequest,
Self::ApiResponse,
Self::McpInvocation,
Self::ExternalLink,
Self::Iframe,
Self::DatabaseQuery,
Self::CrossService,
Self::SubprocessLaunch,
Self::DependencyImport,
Self::UserInput,
Self::FilesystemPath,
Self::EnvironmentVariable,
Self::ShellArgument,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mucosal_kind_str_roundtrip_all_13() {
let all = MucosalKind::all();
assert_eq!(all.len(), 13, "sealed set must be exactly 13 variants");
for variant in all {
let s = variant.as_str();
let back = MucosalKind::parse_kind(s).expect("kebab roundtrip");
assert_eq!(back, variant);
}
}
#[test]
fn mucosal_kind_parses_all_forms() {
assert_eq!(
MucosalKind::parse_kind("MucosalKind::UserInput"),
Some(MucosalKind::UserInput)
);
assert_eq!(
MucosalKind::parse_kind("user_input"),
Some(MucosalKind::UserInput)
);
assert_eq!(
MucosalKind::parse_kind("UserInput"),
Some(MucosalKind::UserInput)
);
assert_eq!(
MucosalKind::parse_kind("user-input"),
Some(MucosalKind::UserInput)
);
assert_eq!(MucosalKind::parse_kind("unknown"), None);
}
#[test]
fn mucosal_kind_rejects_removed_variants() {
assert_eq!(MucosalKind::parse_kind("PrBoundary"), None);
assert_eq!(MucosalKind::parse_kind("pr-boundary"), None);
assert_eq!(MucosalKind::parse_kind("Import"), None);
assert_eq!(MucosalKind::parse_kind("import"), None);
}
#[test]
fn mucosal_kind_serde_kebab_case() {
let s = serde_json::to_string(&MucosalKind::DatabaseQuery).unwrap();
assert_eq!(s, "\"database-query\"");
let v: MucosalKind = serde_json::from_str("\"shell-argument\"").unwrap();
assert_eq!(v, MucosalKind::ShellArgument);
}
}