use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;
pub const MAX_SERVER_ID_LENGTH: usize = 64;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct GenerateSkillParams {
#[schemars(length(max = 64), regex(pattern = r"^[a-z0-9-]+$"))]
pub server_id: String,
pub servers_dir: Option<PathBuf>,
pub skill_name: Option<String>,
pub use_case_hints: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GenerateSkillResult {
pub server_id: String,
pub skill_name: String,
pub server_description: Option<String>,
pub categories: Vec<SkillCategory>,
pub tool_count: usize,
pub example_tools: Vec<ToolExample>,
pub generation_prompt: String,
pub output_path: String,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SkillCategory {
pub name: String,
pub display_name: String,
pub tools: Vec<SkillTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SkillTool {
pub name: String,
pub typescript_name: String,
pub description: String,
pub keywords: Vec<String>,
pub required_params: Vec<String>,
pub optional_params: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ToolExample {
pub tool_name: String,
pub description: String,
pub cli_command: String,
pub params_json: String,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SaveSkillParams {
#[schemars(length(max = 64), regex(pattern = r"^[a-z0-9-]+$"))]
pub server_id: String,
#[schemars(length(max = 102_400))]
pub content: String,
pub output_path: Option<PathBuf>,
#[serde(default)]
pub overwrite: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SaveSkillResult {
pub success: bool,
pub output_path: String,
pub overwritten: bool,
pub metadata: SkillMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
pub section_count: usize,
pub word_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum SkillServerIdError {
#[error("server_id must not be empty")]
Empty,
#[error("server_id too long: {len} chars exceeds {limit} limit")]
TooLong {
len: usize,
limit: usize,
},
#[error("server_id must contain only lowercase letters, digits, and hyphens")]
InvalidCharacters,
}
pub fn validate_server_id(server_id: &str) -> Result<(), SkillServerIdError> {
if server_id.is_empty() {
return Err(SkillServerIdError::Empty);
}
if server_id.len() > MAX_SERVER_ID_LENGTH {
return Err(SkillServerIdError::TooLong {
len: server_id.len(),
limit: MAX_SERVER_ID_LENGTH,
});
}
if !server_id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(SkillServerIdError::InvalidCharacters);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_skill_params_schema_declares_server_id_bounds() {
let schema = schemars::schema_for!(GenerateSkillParams);
let props = schema.get("properties").unwrap().as_object().unwrap();
assert_eq!(props["server_id"]["maxLength"], MAX_SERVER_ID_LENGTH);
assert_eq!(props["server_id"]["pattern"], "^[a-z0-9-]+$");
}
#[test]
fn test_save_skill_params_schema_declares_bounds() {
let schema = schemars::schema_for!(SaveSkillParams);
let props = schema.get("properties").unwrap().as_object().unwrap();
assert_eq!(props["server_id"]["maxLength"], MAX_SERVER_ID_LENGTH);
assert_eq!(props["server_id"]["pattern"], "^[a-z0-9-]+$");
assert_eq!(props["content"]["maxLength"], 102_400);
}
#[test]
fn test_validate_server_id_valid() {
assert!(validate_server_id("github").is_ok());
assert!(validate_server_id("my-server").is_ok());
assert!(validate_server_id("server123").is_ok());
assert!(validate_server_id("my-server-123").is_ok());
}
#[test]
fn test_validate_server_id_empty() {
let result = validate_server_id("");
assert_eq!(result, Err(SkillServerIdError::Empty));
}
#[test]
fn test_validate_server_id_uppercase() {
let result = validate_server_id("GitHub");
assert_eq!(result, Err(SkillServerIdError::InvalidCharacters));
}
#[test]
fn test_validate_server_id_underscore() {
let result = validate_server_id("my_server");
assert_eq!(result, Err(SkillServerIdError::InvalidCharacters));
}
#[test]
fn test_validate_server_id_special_chars() {
let result = validate_server_id("my@server");
assert_eq!(result, Err(SkillServerIdError::InvalidCharacters));
}
#[test]
fn test_validate_server_id_too_long() {
let long_id = "a".repeat(65);
let result = validate_server_id(&long_id);
assert_eq!(
result,
Err(SkillServerIdError::TooLong {
len: 65,
limit: MAX_SERVER_ID_LENGTH
})
);
}
#[test]
fn test_validate_server_id_max_length() {
let max_id = "a".repeat(64);
assert!(validate_server_id(&max_id).is_ok());
}
}