use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum SkillFormat {
#[default]
Legacy,
SkillMd,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillDefinition {
pub name: String,
pub description: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub variables: Vec<String>,
#[serde(default)]
pub argument_hint: Option<String>,
#[serde(default)]
pub allowed_tools: Vec<String>,
#[serde(default)]
pub user_invocable: bool,
#[serde(default)]
pub disable_model_invocation: bool,
#[serde(skip)]
pub instructions: String,
#[serde(skip)]
pub format: SkillFormat,
#[serde(skip)]
pub source_path: Option<PathBuf>,
#[serde(default, flatten)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl SkillDefinition {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
version: String::new(),
variables: Vec::new(),
argument_hint: None,
allowed_tools: Vec::new(),
user_invocable: false,
disable_model_invocation: false,
instructions: String::new(),
format: SkillFormat::default(),
source_path: None,
metadata: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skill_definition_new() {
let skill = SkillDefinition::new("test", "A test skill");
assert_eq!(skill.name, "test");
assert_eq!(skill.description, "A test skill");
assert!(skill.version.is_empty());
assert!(skill.variables.is_empty());
assert!(skill.instructions.is_empty());
assert_eq!(skill.format, SkillFormat::Legacy);
assert!(skill.source_path.is_none());
}
#[test]
fn skill_format_default() {
assert_eq!(SkillFormat::default(), SkillFormat::Legacy);
}
#[test]
fn skill_definition_serde_roundtrip() {
let mut skill = SkillDefinition::new("roundtrip", "Roundtrip test");
skill.version = "2.0.0".into();
skill.variables = vec!["var1".into(), "var2".into()];
skill.user_invocable = true;
skill.instructions = "These should be skipped".into();
let json = serde_json::to_string(&skill).unwrap();
let restored: SkillDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(restored.name, "roundtrip");
assert_eq!(restored.version, "2.0.0");
assert_eq!(restored.variables, vec!["var1", "var2"]);
assert!(restored.user_invocable);
assert!(restored.instructions.is_empty());
assert_eq!(restored.format, SkillFormat::Legacy);
}
#[test]
fn skill_definition_from_json_with_extras() {
let json = r#"{
"name": "extra",
"description": "Has extra fields",
"custom_field": "custom_value",
"priority": 5
}"#;
let skill: SkillDefinition = serde_json::from_str(json).unwrap();
assert_eq!(skill.name, "extra");
assert_eq!(
skill.metadata.get("custom_field"),
Some(&serde_json::json!("custom_value"))
);
assert_eq!(skill.metadata.get("priority"), Some(&serde_json::json!(5)));
}
}