use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillManifest {
pub name: String,
pub version: semver::Version,
pub description: String,
pub author: String,
pub license: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub dependencies: Vec<SkillDependency>,
#[serde(default)]
pub min_framework_version: Option<semver::VersionReq>,
#[serde(default)]
pub repository: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signing_key: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillDependency {
pub name: String,
pub version_req: semver::VersionReq,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_serde_roundtrip() {
let manifest = SkillManifest {
name: "review-pr".to_string(),
version: semver::Version::new(1, 2, 3),
description: "Reviews pull requests".to_string(),
author: "Test Author".to_string(),
license: "MIT".to_string(),
tags: vec!["code-review".to_string(), "testing".to_string()],
dependencies: vec![SkillDependency {
name: "lint-code".to_string(),
version_req: semver::VersionReq::parse(">=0.5.0").unwrap(),
}],
min_framework_version: Some(semver::VersionReq::parse(">=0.6.0").unwrap()),
repository: Some("https://github.com/example/review-pr".to_string()),
signing_key: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_string(&manifest).unwrap();
let deserialized: SkillManifest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "review-pr");
assert_eq!(deserialized.version, semver::Version::new(1, 2, 3));
assert_eq!(deserialized.tags.len(), 2);
assert_eq!(deserialized.dependencies.len(), 1);
assert_eq!(deserialized.dependencies[0].name, "lint-code");
}
#[test]
fn test_manifest_minimal() {
let json = r#"{
"name": "simple",
"version": "0.10.0",
"description": "A simple skill",
"author": "Me",
"license": "MIT",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}"#;
let manifest: SkillManifest = serde_json::from_str(json).unwrap();
assert_eq!(manifest.name, "simple");
assert!(manifest.tags.is_empty());
assert!(manifest.dependencies.is_empty());
assert!(manifest.min_framework_version.is_none());
assert!(manifest.repository.is_none());
}
#[test]
fn test_semver_parsing() {
let version = semver::Version::parse("1.0.0-beta.1").unwrap();
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
assert_eq!(version.patch, 0);
let req = semver::VersionReq::parse(">=0.6.0, <1.0.0").unwrap();
assert!(req.matches(&semver::Version::new(0, 6, 0)));
assert!(!req.matches(&semver::Version::new(1, 0, 0)));
}
#[test]
fn test_skill_dependency_serde() {
let dep = SkillDependency {
name: "base-skill".to_string(),
version_req: semver::VersionReq::parse("^1.0").unwrap(),
};
let json = serde_json::to_string(&dep).unwrap();
let deserialized: SkillDependency = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "base-skill");
assert!(
deserialized
.version_req
.matches(&semver::Version::new(1, 5, 0))
);
}
}