use crate::http::errors::{HttpError, HttpResult};
use crate::http::handlers::{skill_storage::SkillStorage, AppState};
use crate::http::models::*;
use axum::{
extract::{Multipart, Path, Query, State},
Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use validator::Validate;
#[derive(Debug)]
pub struct MultipartFile {
pub filename: String,
pub content: Vec<u8>,
pub content_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreateSkillVersionResponse {
pub created_at: String,
pub description: String,
pub directory: String,
pub id: String,
pub name: String,
pub skill_id: String,
pub r#type: String, pub version: String,
}
#[derive(Debug, Serialize)]
pub struct ListSkillsResponse {
pub skills: Vec<SkillInfo>,
pub count: usize,
pub total: usize,
}
#[derive(Debug, Serialize)]
pub struct SkillInfo {
pub id: String,
pub name: String,
pub description: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub latest_version: Option<String>,
pub source: String, }
#[derive(Debug, Serialize)]
pub struct GetSkillResponse {
pub id: String,
pub name: String,
pub description: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub latest_version: Option<String>,
pub source: String,
}
#[derive(Debug, Serialize)]
pub struct CreateSkillResponse {
pub id: String,
pub name: String,
pub description: String,
pub created_at: String,
pub source: String,
}
#[derive(Debug, Deserialize)]
pub struct ListSkillsQuery {
pub source: Option<String>, pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub async fn create_skill(
State(state): State<AppState>,
Json(request): Json<CreateSkillRequest>,
) -> HttpResult<Json<ApiResponse<CreateSkillResponse>>> {
request.validate().map_err(|e| {
HttpError::ValidationError(
e.field_errors()
.into_iter()
.map(|(field, errors)| {
(
field.to_string(),
errors
.iter()
.map(|e| e.message.clone().unwrap_or_default().to_string())
.collect(),
)
})
.collect(),
)
})?;
let skill_id = SkillStorage::generate_skill_id();
let skills_dir = state.service.config().skill_storage_path.clone();
let _skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
let created_at = chrono::Utc::now();
let response = CreateSkillResponse {
id: skill_id,
name: request.display_title.clone(),
description: request.description.unwrap_or_default(),
created_at: created_at.to_rfc3339(),
source: "custom".to_string(),
};
Ok(Json(ApiResponse::success(response)))
}
pub async fn list_skills(
State(state): State<AppState>,
Query(query): Query<ListSkillsQuery>,
) -> HttpResult<Json<ApiResponse<ListSkillsResponse>>> {
let skills_dir = state.service.config().skill_storage_path.clone();
let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
let mut skills = skill_storage.list_skills().await?;
if let Some(source_filter) = &query.source {
skills.retain(|skill| {
let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
match source_filter.as_str() {
"anthropic" => is_anthropic,
"custom" => !is_anthropic,
_ => true,
}
});
}
let offset = query.offset.unwrap_or(0);
let limit = query.limit.unwrap_or(50);
let total = skills.len();
let paginated_skills = skills
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
let count = paginated_skills.len();
let skill_infos: Vec<SkillInfo> = paginated_skills
.into_iter()
.map(|skill| {
let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
SkillInfo {
id: skill.id,
name: skill.name,
description: skill.description,
created_at: Some(skill.created_at.to_rfc3339()),
updated_at: Some(skill.updated_at.to_rfc3339()),
latest_version: Some(skill.latest_version),
source: if is_anthropic {
"anthropic".to_string()
} else {
"custom".to_string()
},
}
})
.collect();
let response = ListSkillsResponse {
skills: skill_infos,
count,
total,
};
Ok(Json(ApiResponse::success(response)))
}
pub async fn get_skill(
State(state): State<AppState>,
Path(skill_id): Path<String>,
) -> HttpResult<Json<ApiResponse<GetSkillResponse>>> {
let skills_dir = state.service.config().skill_storage_path.clone();
let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
let skill = skill_storage
.get_skill(&skill_id)
.await?
.ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf");
let response = GetSkillResponse {
id: skill.id,
name: skill.name,
description: skill.description,
created_at: Some(skill.created_at.to_rfc3339()),
updated_at: Some(skill.updated_at.to_rfc3339()),
latest_version: Some(skill.latest_version),
source: if is_anthropic {
"anthropic".to_string()
} else {
"custom".to_string()
},
};
Ok(Json(ApiResponse::success(response)))
}
pub async fn delete_skill(
State(state): State<AppState>,
Path(skill_id): Path<String>,
) -> HttpResult<Json<ApiResponse<serde_json::Value>>> {
let skills_dir = state.service.config().skill_storage_path.clone();
let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
skill_storage.delete_skill(&skill_id).await?;
Ok(Json(ApiResponse::success(serde_json::json!({
"message": "Skill deleted successfully"
}))))
}
pub async fn create_skill_version(
State(state): State<AppState>,
Path(skill_id): Path<String>,
mut multipart: Multipart,
) -> HttpResult<Json<ApiResponse<CreateSkillVersionResponse>>> {
let skills_dir = state.service.config().skill_storage_path.clone();
let skill_storage = SkillStorage::new(state.service.clone(), skills_dir);
let mut files = HashMap::new();
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| HttpError::BadRequest(format!("Failed to read multipart field: {}", e)))?
{
let name = field.name().unwrap_or("unnamed").to_string();
if name == "files" || name == "files[]" {
let filename = field.file_name().unwrap_or("unnamed").to_string();
let content = field.bytes().await.map_err(|e| {
HttpError::BadRequest(format!("Failed to read file content: {}", e))
})?;
files.insert(filename, content.to_vec());
}
}
if files.is_empty() {
return Err(HttpError::BadRequest("No files provided".to_string()));
}
let version = skill_storage.store_skill_version(&skill_id, files).await?;
let response = CreateSkillVersionResponse {
created_at: version.created_at.to_rfc3339(),
description: version.description,
directory: version.directory,
id: version.id,
name: version.name,
skill_id: version.skill_id,
r#type: "skill_version".to_string(),
version: version.version,
};
Ok(Json(ApiResponse::success(response)))
}
pub async fn list_skill_versions(
State(_state): State<AppState>,
Path(_skill_id): Path<String>,
) -> HttpResult<Json<ApiResponse<SkillVersionsListResponse>>> {
let response = SkillVersionsListResponse {
versions: vec![],
count: 0,
total: 0,
};
Ok(Json(ApiResponse::success(response)))
}
pub async fn get_skill_version(
State(_state): State<AppState>,
Path((_skill_id, _version)): Path<(String, String)>,
) -> HttpResult<Json<ApiResponse<SkillVersionResponse>>> {
Err(HttpError::NotFound(format!(
"Skill version not found: {}@{}",
_skill_id, _version
)))
}
pub async fn delete_skill_version(
State(_state): State<AppState>,
Path((_skill_id, _version)): Path<(String, String)>,
) -> HttpResult<Json<ApiResponse<serde_json::Value>>> {
Err(HttpError::NotFound(format!(
"Skill version not found: {}@{}",
_skill_id, _version
)))
}
#[derive(Debug, Serialize)]
pub struct SkillVersionsListResponse {
pub versions: Vec<SkillVersionInfo>,
pub count: usize,
pub total: usize,
}
#[derive(Debug, Serialize)]
pub struct SkillVersionInfo {
pub id: String,
pub version: String,
pub created_at: String,
pub name: String,
pub description: String,
}
#[derive(Debug, Serialize)]
pub struct SkillVersionResponse {
pub id: String,
pub skill_id: String,
pub version: String,
pub created_at: String,
pub name: String,
pub description: String,
pub directory: String,
pub r#type: String,
}
#[derive(Debug, Deserialize, validator::Validate)]
pub struct CreateSkillRequest {
#[validate(length(min = 1, max = 64))]
pub display_title: String,
#[validate(length(max = 1024))]
pub description: Option<String>,
}