use proc_macro2::Ident;
use syn::Type;
#[derive(Debug)]
#[allow(dead_code)]
pub struct RouterDef {
pub struct_type: syn::Path,
pub generics: Option<syn::Generics>,
pub tools: Vec<ToolDef>,
pub metadata: RouterMetadata,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ToolDef {
pub method_name: Ident,
pub tool_name: String,
pub params: Vec<ParamDef>,
pub return_type: Type,
pub metadata: ToolMetadata,
pub is_async: bool,
pub visibility: syn::Visibility,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ParamDef {
pub name: Ident,
pub ty: Type,
pub source: ParamSource,
pub is_optional: bool,
pub metadata: ParamMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParamSource {
#[default]
Body,
Query,
Path,
Header,
}
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct RouterMetadata {
pub openapi_tag: Option<String>,
pub base_path: Option<String>,
pub cli_config: Option<RouterCliConfig>,
pub mcp_config: Option<RouterMcpConfig>,
pub rest_config: Option<RouterRestConfig>,
}
#[derive(Debug, Default)]
pub struct RouterCliConfig {
pub name: Option<String>,
pub description: Option<String>,
pub global_output_formats: Vec<String>,
pub standard_global_args: bool,
}
#[derive(Debug, Default)]
pub struct RouterMcpConfig {
pub name: Option<String>,
pub version: Option<String>,
}
#[derive(Debug, Default)]
pub struct RouterRestConfig {
pub prefix: Option<String>,
}
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct ToolMetadata {
pub description: String,
pub short_description: Option<String>,
pub rest_config: Option<RestConfig>,
pub mcp_config: Option<McpConfig>,
pub cli_config: Option<CliConfig>,
}
#[derive(Debug)]
pub struct RestConfig {
pub path: Option<String>,
pub method: HttpMethod,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HttpMethod {
Get,
#[default]
Post,
Put,
Delete,
Patch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpOutputMode {
Json,
Text,
}
#[derive(Debug, Default)]
pub struct McpConfig {
pub annotations: McpAnnotations,
pub output_mode: Option<McpOutputMode>,
}
#[derive(Debug, Default)]
pub struct McpAnnotations {
pub read_only_hint: Option<bool>,
pub destructive_hint: Option<bool>,
pub idempotent_hint: Option<bool>,
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct CliConfig {
pub name: Option<String>,
pub aliases: Vec<String>,
pub hidden: bool,
pub output_formats: Vec<String>,
pub progress_style: Option<String>,
pub examples: Vec<CliExample>,
pub supports_stdin: bool,
pub supports_stdout: bool,
pub confirm: Option<String>,
pub interactive: bool,
pub command_path: Vec<String>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CliExample {
pub command: String,
pub description: String,
}
#[derive(Debug, Default, Clone)]
#[allow(dead_code)]
pub struct ParamMetadata {
pub description: Option<String>,
pub short: Option<char>,
pub long: Option<String>,
pub env: Option<String>,
pub default: Option<String>,
pub possible_values: Vec<String>,
pub completions: Option<String>,
pub multiple: bool,
pub delimiter: Option<char>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ValidationError {
pub span: proc_macro2::Span,
pub message: String,
pub help: Option<String>,
}
impl RouterDef {
pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
let mut tool_names = std::collections::HashSet::new();
for tool in &self.tools {
if !tool_names.insert(&tool.tool_name) {
errors.push(ValidationError {
span: tool.method_name.span(),
message: format!("Duplicate tool name: {}", tool.tool_name),
help: Some(
"Use the 'name' attribute to specify a different tool name".to_string(),
),
});
}
}
for tool in &self.tools {
if let Err(tool_errors) = tool.validate() {
errors.extend(tool_errors);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl ToolDef {
fn extract_path_params(&self, path: &str) -> Vec<String> {
let mut params = Vec::new();
for segment in path.split('/') {
if let Some(stripped) = segment.strip_prefix(':') {
params.push(stripped.to_string());
}
}
params
}
pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if !self.is_valid_return_type() {
errors.push(ValidationError {
span: proc_macro2::Span::call_site(),
message: "Tool methods must return Result<T, ToolError>".to_string(),
help: Some("Change your return type to Result<YourType, ToolError>".to_string()),
});
}
let mut param_names = std::collections::HashSet::new();
for param in &self.params {
if !param_names.insert(¶m.name) {
errors.push(ValidationError {
span: param.name.span(),
message: format!("Duplicate parameter name: {}", param.name),
help: None,
});
}
}
if let Some(rest_config) = &self.metadata.rest_config {
let path_params = if let Some(path) = &rest_config.path {
self.extract_path_params(path)
} else {
Vec::new()
};
if rest_config.method == HttpMethod::Get {
for param in &self.params {
if path_params.contains(¶m.name.to_string()) {
continue;
}
if param.source == ParamSource::Body {
errors.push(ValidationError {
span: param.name.span(),
message: "GET requests cannot have body parameters".to_string(),
help: Some("Use #[universal_tool_param(source = \"query\")] or change the HTTP method".to_string()),
});
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn is_valid_return_type(&self) -> bool {
true
}
}