use serde::{Deserialize, Serialize};
use crate::lua_skill::{validate_luaskills_identifier, validate_luaskills_version};
use crate::skill::source::SkillInstallSourceType;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillSourceManifest {
#[serde(default)]
pub schema_version: Option<u32>,
pub skill_id: String,
pub version: String,
pub archive: SkillSourceArchive,
#[serde(default)]
pub update: Option<SkillSourceUpdate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillSourceArchive {
#[serde(rename = "type", alias = "archive_type")]
pub archive_type: String,
pub url: String,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillSourceUpdate {
pub source_type: SkillInstallSourceType,
pub locator: String,
#[serde(default)]
pub tag: Option<String>,
}
impl SkillSourceManifest {
pub fn validate_for_skill(&self, expected_skill_id: &str) -> Result<String, String> {
validate_luaskills_identifier(&self.skill_id, "source manifest skill_id")?;
validate_luaskills_identifier(expected_skill_id, "expected skill_id")?;
if self.skill_id != expected_skill_id {
return Err(format!(
"source manifest resolves to skill_id '{}' instead of '{}'",
self.skill_id, expected_skill_id
));
}
validate_luaskills_version(&self.version, "source manifest version")?;
if !self.archive.archive_type.trim().eq_ignore_ascii_case("zip") {
return Err(format!(
"source manifest archive type '{}' is not supported; expected zip",
self.archive.archive_type
));
}
if self.archive.url.trim().is_empty() {
return Err("source manifest archive.url must not be empty".to_string());
}
normalize_sha256(self.archive.sha256.as_str())
}
}
pub fn parse_skill_source_manifest(
content: &str,
origin: &str,
) -> Result<SkillSourceManifest, String> {
serde_yaml::from_str::<SkillSourceManifest>(content).map_err(|error| {
format!(
"Failed to parse skill source manifest from {}: {}",
origin, error
)
})
}
pub fn normalize_sha256(value: &str) -> Result<String, String> {
let normalized = value.trim().to_ascii_lowercase();
if normalized.len() == 64 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
return Ok(normalized);
}
Err("source manifest archive.sha256 must be one valid SHA-256 value".to_string())
}