use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use crate::dependency::types::{DependencyScope, DependencySourceType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DependencyArchiveType {
Zip,
TarGz,
#[default]
Raw,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencyExportSpec {
pub archive_path: String,
pub target_path: String,
#[serde(default)]
pub executable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencyPackageSpec {
#[serde(default)]
pub archive_type: DependencyArchiveType,
#[serde(default)]
pub asset_name: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub exports: Vec<DependencyExportSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GithubReleaseSourceSpec {
pub repo: String,
#[serde(default)]
pub tag_api: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UrlSourceSpec {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillListSourceSpec {
pub url: String,
pub package: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencySourceSpec {
#[serde(rename = "type")]
pub source_type: DependencySourceType,
#[serde(default)]
pub github: Option<GithubReleaseSourceSpec>,
#[serde(default)]
pub url: Option<UrlSourceSpec>,
#[serde(default)]
pub skilllist: Option<SkillListSourceSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDependencySpec {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default = "default_required_dependency")]
pub required: bool,
#[serde(default = "default_tool_dependency_scope")]
pub scope: DependencyScope,
pub source: DependencySourceSpec,
#[serde(default)]
pub packages: BTreeMap<String, DependencyPackageSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LuaDependencySpec {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default = "default_required_dependency")]
pub required: bool,
#[serde(default = "default_runtime_library_scope")]
pub scope: DependencyScope,
pub source: DependencySourceSpec,
#[serde(default)]
pub packages: BTreeMap<String, DependencyPackageSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FfiDependencySpec {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default = "default_required_dependency")]
pub required: bool,
#[serde(default = "default_runtime_library_scope")]
pub scope: DependencyScope,
pub source: DependencySourceSpec,
#[serde(default)]
pub packages: BTreeMap<String, DependencyPackageSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SkillDependencyManifest {
#[serde(default)]
pub tool_dependencies: Vec<ToolDependencySpec>,
#[serde(default)]
pub lua_dependencies: Vec<LuaDependencySpec>,
#[serde(default)]
pub ffi_dependencies: Vec<FfiDependencySpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillListPackageManifest {
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub packages: BTreeMap<String, DependencyPackageSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SkillListIndexFile {
#[serde(default)]
pub packages: BTreeMap<String, SkillListPackageManifest>,
}
fn default_required_dependency() -> bool {
true
}
fn default_tool_dependency_scope() -> DependencyScope {
DependencyScope::Skill
}
fn default_runtime_library_scope() -> DependencyScope {
DependencyScope::Skill
}
impl SkillDependencyManifest {
pub fn load_from_path(path: &Path) -> Result<Self, String> {
let yaml_text = fs::read_to_string(path)
.map_err(|error| format!("Failed to read {}: {}", path.display(), error))?;
serde_yaml::from_str(&yaml_text)
.map_err(|error| format!("Failed to parse {}: {}", path.display(), error))
}
pub fn is_empty(&self) -> bool {
self.tool_dependencies.is_empty()
&& self.lua_dependencies.is_empty()
&& self.ffi_dependencies.is_empty()
}
}
impl ToolDependencySpec {
pub fn package_for_platform(&self, platform_key: &str) -> Option<&DependencyPackageSpec> {
self.packages.get(platform_key)
}
}
impl LuaDependencySpec {
pub fn package_for_platform(&self, platform_key: &str) -> Option<&DependencyPackageSpec> {
self.packages.get(platform_key)
}
}
impl FfiDependencySpec {
pub fn package_for_platform(&self, platform_key: &str) -> Option<&DependencyPackageSpec> {
self.packages.get(platform_key)
}
}
#[cfg(test)]
mod tests {
use super::SkillDependencyManifest;
#[test]
fn parse_dependency_manifest_groups() {
let yaml_text = r#"
tool_dependencies:
- name: ast-grep
scope: skill
source:
type: github_release
github:
repo: ast-grep/ast-grep
packages:
windows-x64:
archive_type: zip
asset_name: app-x86_64-pc-windows-msvc.zip
exports:
- archive_path: ast-grep.exe
target_path: bin/ast-grep.exe
lua_dependencies:
- name: lua-cjson
required: false
scope: skill
source:
type: url
url: {}
packages:
windows-x64:
archive_type: raw
url: https://example.com/cjson.lua
exports:
- archive_path: cjson.lua
target_path: share/lua/cjson.lua
ffi_dependencies:
- name: example-lib
source:
type: skilllist
skilllist:
url: https://example.com/index.yaml
package: example-lib
"#;
let manifest: SkillDependencyManifest =
serde_yaml::from_str(yaml_text).expect("manifest should parse");
assert_eq!(manifest.tool_dependencies.len(), 1);
assert_eq!(manifest.lua_dependencies.len(), 1);
assert_eq!(manifest.ffi_dependencies.len(), 1);
assert_eq!(
manifest.tool_dependencies[0].scope,
crate::dependency::types::DependencyScope::Skill
);
assert_eq!(
manifest.lua_dependencies[0].scope,
crate::dependency::types::DependencyScope::Skill
);
assert_eq!(
manifest.tool_dependencies[0]
.package_for_platform("windows-x64")
.expect("windows package should exist")
.exports[0]
.target_path,
"bin/ast-grep.exe"
);
}
#[test]
fn dependency_scope_defaults_match_kind_policy() {
let yaml_text = r#"
tool_dependencies:
- name: rg
source:
type: url
url: {}
packages: {}
lua_dependencies:
- name: demo-lua
source:
type: url
url: {}
packages: {}
ffi_dependencies:
- name: demo-ffi
source:
type: url
url: {}
packages: {}
"#;
let manifest: SkillDependencyManifest =
serde_yaml::from_str(yaml_text).expect("manifest should parse");
assert_eq!(
manifest.tool_dependencies[0].scope,
crate::dependency::types::DependencyScope::Skill
);
assert_eq!(
manifest.lua_dependencies[0].scope,
crate::dependency::types::DependencyScope::Skill
);
assert_eq!(
manifest.ffi_dependencies[0].scope,
crate::dependency::types::DependencyScope::Skill
);
}
}