Skip to main content

sim_lib_surface_card/
name.rs

1//! External-name policies for projecting kernel symbols onto foreign surfaces.
2
3use sim_kernel::Symbol;
4
5/// Policy selecting how a kernel [`Symbol`] is mangled into a surface-specific
6/// external name.
7///
8/// Each variant fails closed toward the character set its surface accepts; none
9/// of them is reversible, so they are used for naming only, never for routing.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExternalNamePolicy {
12    /// MCP tool name: keep `[A-Za-z0-9_-]`, replace everything else (including
13    /// `/` and `.`) with a single `_` per character. No run-collapsing, no trim.
14    McpTool,
15    /// OpenAI tool name: keep ascii-alphanumeric characters; collapse any run of
16    /// non-alphanumeric characters to a single `_`; then trim leading/trailing
17    /// `_`. May return an empty string (callers supply their own fallback).
18    OpenAiTool,
19    /// File-safe name: keep ascii-alphanumeric and `-`, `_`, `.`; replace every
20    /// other character with `_` per character. No run-collapsing, no trim.
21    FileSafe,
22    /// Human-readable name: the qualified symbol string unchanged.
23    HumanReadable,
24}
25
26/// Project `symbol` onto an external name according to `policy`.
27pub fn external_name(symbol: &Symbol, policy: ExternalNamePolicy) -> String {
28    let qualified = symbol.as_qualified_str();
29    match policy {
30        ExternalNamePolicy::OpenAiTool => openai_tool(&qualified),
31        ExternalNamePolicy::FileSafe => map_chars(&qualified, |ch| matches!(ch, '-' | '_' | '.')),
32        ExternalNamePolicy::McpTool => map_chars(&qualified, |ch| matches!(ch, '-' | '_')),
33        ExternalNamePolicy::HumanReadable => qualified,
34    }
35}
36
37/// Builds the OpenAI tool name for a qualified symbol, minus the empty-string
38/// fallback (the caller keeps that).
39fn openai_tool(qualified: &str) -> String {
40    let mut out = String::new();
41    let mut last_was_separator = false;
42    for ch in qualified.chars() {
43        if ch.is_ascii_alphanumeric() {
44            out.push(ch);
45            last_was_separator = false;
46        } else if !last_was_separator {
47            out.push('_');
48            last_was_separator = true;
49        }
50    }
51    out.trim_matches('_').to_owned()
52}
53
54/// Per-character map: keep ascii-alphanumeric and any character `keep_extra`
55/// accepts; replace everything else with a single `_`.
56fn map_chars(qualified: &str, keep_extra: impl Fn(char) -> bool) -> String {
57    qualified
58        .chars()
59        .map(|ch| {
60            if ch.is_ascii_alphanumeric() || keep_extra(ch) {
61                ch
62            } else {
63                '_'
64            }
65        })
66        .collect()
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn policies_on_qualified_symbol() {
75        let symbol = Symbol::qualified("skill", "do_thing/v2");
76        // as_qualified_str() == "skill/do_thing/v2"
77        assert_eq!(
78            external_name(&symbol, ExternalNamePolicy::OpenAiTool),
79            "skill_do_thing_v2"
80        );
81        assert_eq!(
82            external_name(&symbol, ExternalNamePolicy::FileSafe),
83            "skill_do_thing_v2"
84        );
85        assert_eq!(
86            external_name(&symbol, ExternalNamePolicy::McpTool),
87            "skill_do_thing_v2"
88        );
89        assert_eq!(
90            external_name(&symbol, ExternalNamePolicy::HumanReadable),
91            "skill/do_thing/v2"
92        );
93    }
94
95    #[test]
96    fn openai_tool_empty_case() {
97        // A symbol whose qualified string contains no ascii-alphanumeric
98        // characters mangles to the empty string under OpenAiTool.
99        let symbol = Symbol::qualified("--", "//");
100        assert_eq!(external_name(&symbol, ExternalNamePolicy::OpenAiTool), "");
101    }
102}