use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::{Arc, RwLock};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ToolConfig {
Desc(String),
Tags(Vec<String>),
ParamDesc {
name: String,
desc: String,
},
ParamExample {
name: String,
example: serde_json::Value,
},
ParamDefault {
name: String,
default: serde_json::Value,
},
ParamRequired {
name: String,
required: bool,
},
ParamMin {
name: String,
min: f64,
},
ParamMax {
name: String,
max: f64,
},
ParamMinLength {
name: String,
min_length: u64,
},
ParamMaxLength {
name: String,
max_length: u64,
},
ParamPattern {
name: String,
pattern: String,
},
ParamMinItems {
name: String,
min_items: u64,
},
ParamMaxItems {
name: String,
max_items: u64,
},
ParamMultipleOf {
name: String,
multiple_of: f64,
},
}
impl ToolConfig {
pub fn desc<S: Into<String>>(desc: S) -> Self {
ToolConfig::Desc(desc.into())
}
pub fn tags(tags: Vec<String>) -> Self {
ToolConfig::Tags(tags)
}
pub fn param_desc<N: Into<String>, D: Into<String>>(name: N, desc: D) -> Self {
ToolConfig::ParamDesc {
name: name.into(),
desc: desc.into(),
}
}
pub fn param_example<N: Into<String>>(name: N, example: serde_json::Value) -> Self {
ToolConfig::ParamExample {
name: name.into(),
example,
}
}
pub fn param_default<N: Into<String>>(name: N, default: serde_json::Value) -> Self {
ToolConfig::ParamDefault {
name: name.into(),
default,
}
}
pub fn param_required<N: Into<String>>(name: N, required: bool) -> Self {
ToolConfig::ParamRequired {
name: name.into(),
required,
}
}
pub fn param_min<N: Into<String>>(name: N, min: f64) -> Self {
ToolConfig::ParamMin {
name: name.into(),
min,
}
}
pub fn param_max<N: Into<String>>(name: N, max: f64) -> Self {
ToolConfig::ParamMax {
name: name.into(),
max,
}
}
pub fn param_min_length<N: Into<String>>(name: N, min_length: u64) -> Self {
ToolConfig::ParamMinLength {
name: name.into(),
min_length,
}
}
pub fn param_max_length<N: Into<String>>(name: N, max_length: u64) -> Self {
ToolConfig::ParamMaxLength {
name: name.into(),
max_length,
}
}
pub fn param_pattern<N: Into<String>, P: Into<String>>(name: N, pattern: P) -> Self {
ToolConfig::ParamPattern {
name: name.into(),
pattern: pattern.into(),
}
}
pub fn param_min_items<N: Into<String>>(name: N, min_items: u64) -> Self {
ToolConfig::ParamMinItems {
name: name.into(),
min_items,
}
}
pub fn param_max_items<N: Into<String>>(name: N, max_items: u64) -> Self {
ToolConfig::ParamMaxItems {
name: name.into(),
max_items,
}
}
pub fn param_multiple_of<N: Into<String>>(name: N, multiple_of: f64) -> Self {
ToolConfig::ParamMultipleOf {
name: name.into(),
multiple_of,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConfigLayer {
ToolAttrDesc = 0,
DocComment = 1,
TokitaiConfig = 2,
Default = 3,
}
impl ConfigLayer {
pub const fn priority(self) -> u8 {
self as u8
}
pub const fn label(self) -> &'static str {
match self {
ConfigLayer::ToolAttrDesc => "#[tool(desc = \"...\")]",
ConfigLayer::DocComment => "doc comment",
ConfigLayer::TokitaiConfig => "tokitai! config",
ConfigLayer::Default => "synthesized default",
}
}
}
pub const CONFIG_PRIORITY_ORDER: [ConfigLayer; 4] = [
ConfigLayer::ToolAttrDesc,
ConfigLayer::DocComment,
ConfigLayer::TokitaiConfig,
ConfigLayer::Default,
];
pub const fn config_priority_table_md() -> [&'static str; 4] {
[
"1. `#[tool(desc = \"...\")]` (compile-time, attribute-supplied) — **wins** on conflict",
"2. doc comment (`///` lines above the method) — used if no `#[tool(desc)]` is present",
"3. tokitai! config block (`GLOBAL_CONFIG_REGISTRY`) — does **not** override an explicit `#[tool(desc)]`",
"4. synthesized default (e.g. `\"调用 <method> 方法\"`) — last-resort fallback",
]
}
pub const fn can_override(compile_time_winner: u8, runtime_layer: ConfigLayer) -> bool {
runtime_layer.priority() < compile_time_winner
}
#[derive(Debug, Default, Clone)]
pub struct ToolConfigRegistry {
configs: Arc<RwLock<HashMap<String, Vec<ToolConfig>>>>,
}
impl ToolConfigRegistry {
pub fn new() -> Self {
Self {
configs: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn configure(&self, tool_name: &str, configs: &[ToolConfig]) {
let mut map = self.configs.write().unwrap();
let entry = map.entry(tool_name.to_string()).or_default();
entry.extend_from_slice(configs);
}
pub fn get(&self, tool_name: &str) -> Vec<ToolConfig> {
let map = self.configs.read().unwrap();
map.get(tool_name).cloned().unwrap_or_default()
}
pub fn has_config(&self, tool_name: &str) -> bool {
let map = self.configs.read().unwrap();
map.contains_key(tool_name)
}
pub fn clear(&self, tool_name: &str) {
let mut map = self.configs.write().unwrap();
map.remove(tool_name);
}
pub fn clear_all(&self) {
let mut map = self.configs.write().unwrap();
map.clear();
}
}
pub static GLOBAL_CONFIG_REGISTRY: LazyLock<ToolConfigRegistry> =
LazyLock::new(ToolConfigRegistry::new);
#[macro_export]
macro_rules! assert_no_config_deadlock {
() => {
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_config_desc() {
let config = ToolConfig::desc("Test description");
match config {
ToolConfig::Desc(desc) => assert_eq!(desc, "Test description"),
_ => panic!("Expected Desc variant"),
}
}
#[test]
fn test_tool_config_tags() {
let config = ToolConfig::tags(vec!["tag1".to_string(), "tag2".to_string()]);
match config {
ToolConfig::Tags(tags) => {
assert_eq!(tags.len(), 2);
assert_eq!(tags[0], "tag1");
assert_eq!(tags[1], "tag2");
}
_ => panic!("Expected Tags variant"),
}
}
#[test]
fn test_tool_config_param_desc() {
let config = ToolConfig::param_desc("param1", "Parameter description");
match config {
ToolConfig::ParamDesc { name, desc } => {
assert_eq!(name, "param1");
assert_eq!(desc, "Parameter description");
}
_ => panic!("Expected ParamDesc variant"),
}
}
#[test]
fn test_tool_config_param_example() {
let example = serde_json::json!("example_value");
let config = ToolConfig::param_example("param1", example.clone());
match config {
ToolConfig::ParamExample { name, example: ex } => {
assert_eq!(name, "param1");
assert_eq!(ex, example);
}
_ => panic!("Expected ParamExample variant"),
}
}
#[test]
fn test_registry_configure_and_get() {
let registry = ToolConfigRegistry::default();
registry.configure(
"test_tool",
&[
ToolConfig::desc("Test description"),
ToolConfig::param_desc("id", "ID parameter"),
],
);
let configs = registry.get("test_tool");
assert_eq!(configs.len(), 2);
}
#[test]
fn test_registry_has_config() {
let registry = ToolConfigRegistry::default();
assert!(!registry.has_config("nonexistent"));
registry.configure("test_tool", &[ToolConfig::desc("Test")]);
assert!(registry.has_config("test_tool"));
}
#[test]
fn test_registry_clear() {
let registry = ToolConfigRegistry::default();
registry.configure("test_tool", &[ToolConfig::desc("Test")]);
assert!(registry.has_config("test_tool"));
registry.clear("test_tool");
assert!(!registry.has_config("test_tool"));
}
#[test]
fn test_registry_clear_all() {
let registry = ToolConfigRegistry::default();
registry.configure("tool1", &[ToolConfig::desc("Test 1")]);
registry.configure("tool2", &[ToolConfig::desc("Test 2")]);
registry.clear_all();
assert!(!registry.has_config("tool1"));
assert!(!registry.has_config("tool2"));
}
#[test]
fn test_registry_multiple_configure() {
let registry = ToolConfigRegistry::default();
registry.configure("test_tool", &[ToolConfig::desc("First")]);
registry.configure("test_tool", &[ToolConfig::param_desc("id", "ID")]);
let configs = registry.get("test_tool");
assert_eq!(configs.len(), 2);
}
#[test]
fn test_global_registry() {
GLOBAL_CONFIG_REGISTRY.configure("global_test", &[ToolConfig::desc("Global config")]);
assert!(GLOBAL_CONFIG_REGISTRY.has_config("global_test"));
GLOBAL_CONFIG_REGISTRY.clear("global_test");
assert!(!GLOBAL_CONFIG_REGISTRY.has_config("global_test"));
}
#[test]
fn test_priority_order_array_matches_expected_ordering() {
assert_eq!(
CONFIG_PRIORITY_ORDER,
[
ConfigLayer::ToolAttrDesc,
ConfigLayer::DocComment,
ConfigLayer::TokitaiConfig,
ConfigLayer::Default,
]
);
}
#[test]
fn test_priority_table_md_contains_every_layer_label() {
let table = config_priority_table_md();
for layer in &CONFIG_PRIORITY_ORDER {
let needle = layer.label();
assert!(
table.iter().any(|row| row.contains(needle)),
"rendered table is missing label {:?}: {:?}",
needle,
table,
);
}
assert_eq!(table.len(), CONFIG_PRIORITY_ORDER.len());
}
#[test]
fn test_can_override_compile_time_attr_desc_is_locked() {
assert!(!can_override(
ConfigLayer::ToolAttrDesc.priority(),
ConfigLayer::TokitaiConfig,
));
}
#[test]
fn test_can_override_doc_comment_beats_runtime() {
assert!(!can_override(
ConfigLayer::DocComment.priority(),
ConfigLayer::TokitaiConfig,
));
}
#[test]
fn test_can_override_default_is_always_open() {
assert!(can_override(
ConfigLayer::Default.priority(),
ConfigLayer::TokitaiConfig,
));
}
#[test]
fn test_config_layer_priority_is_strictly_monotonic() {
for w in CONFIG_PRIORITY_ORDER.windows(2) {
assert!(
w[0].priority() < w[1].priority(),
"{:?} ({}) should outrank {:?} ({})",
w[0],
w[0].priority(),
w[1],
w[1].priority(),
);
}
}
}