use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use astrid_core::UplinkProfile;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapsuleManifest {
pub package: PackageDef,
#[serde(default, rename = "component")]
pub components: Vec<ComponentDef>,
#[serde(default)]
pub imports: ImportsMap,
#[serde(default)]
pub exports: ExportsMap,
#[serde(default)]
pub capabilities: CapabilitiesDef,
#[serde(default)]
pub env: HashMap<String, EnvDef>,
#[serde(default, rename = "context_file")]
pub context_files: Vec<ContextFileDef>,
#[serde(default, rename = "command")]
pub commands: Vec<CommandDef>,
#[serde(default, rename = "mcp_server")]
pub mcp_servers: Vec<McpServerDef>,
#[serde(default, rename = "skill")]
pub skills: Vec<SkillDef>,
#[serde(default, rename = "uplink")]
pub uplinks: Vec<UplinkDef>,
#[serde(default, rename = "interceptor")]
pub interceptors: Vec<InterceptorDef>,
#[serde(default, rename = "topic")]
pub topics: Vec<TopicDef>,
}
impl CapsuleManifest {
#[must_use]
pub fn has_imports(&self) -> bool {
self.imports.values().any(|ns| !ns.is_empty())
}
#[must_use]
pub fn has_exports(&self) -> bool {
self.exports.values().any(|ns| !ns.is_empty())
}
pub fn export_triples(&self) -> impl Iterator<Item = (&str, &str, &semver::Version)> {
self.exports.iter().flat_map(|(ns, ifaces)| {
ifaces
.iter()
.map(move |(name, def)| (ns.as_str(), name.as_str(), &def.version))
})
}
pub fn import_tuples(&self) -> impl Iterator<Item = (&str, &str, &semver::VersionReq, bool)> {
self.imports.iter().flat_map(|(ns, ifaces)| {
ifaces
.iter()
.map(move |(name, def)| (ns.as_str(), name.as_str(), &def.version, def.optional))
})
}
}
pub type ImportsMap = HashMap<String, HashMap<String, ImportDef>>;
pub type ExportsMap = HashMap<String, HashMap<String, ExportDef>>;
#[derive(Debug, Clone, Serialize)]
pub struct ImportDef {
pub version: semver::VersionReq,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub optional: bool,
}
impl<'de> Deserialize<'de> for ImportDef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Short(String),
Full {
version: String,
#[serde(default)]
optional: bool,
},
}
let raw = Raw::deserialize(deserializer)?;
let (version_str, optional) = match raw {
Raw::Short(s) => (s, false),
Raw::Full { version, optional } => (version, optional),
};
let version = semver::VersionReq::parse(&version_str).map_err(|e| {
serde::de::Error::custom(format!("invalid semver requirement '{version_str}': {e}"))
})?;
Ok(Self { version, optional })
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ExportDef {
pub version: semver::Version,
}
impl<'de> Deserialize<'de> for ExportDef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Short(String),
Full { version: String },
}
let raw = Raw::deserialize(deserializer)?;
let version_str = match raw {
Raw::Short(s) => s,
Raw::Full { version } => version,
};
let version = semver::Version::parse(&version_str).map_err(|e| {
serde::de::Error::custom(format!("invalid semver version '{version_str}': {e}"))
})?;
Ok(Self { version })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageDef {
pub name: String,
pub version: String,
pub description: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub license: Option<String>,
#[serde(rename = "license-file")]
pub license_file: Option<PathBuf>,
pub readme: Option<PathBuf>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub categories: Vec<String>,
#[serde(rename = "astrid-version")]
pub astrid_version: Option<String>,
pub publish: Option<bool>,
pub include: Option<Vec<String>>,
pub exclude: Option<Vec<String>>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentDef {
#[serde(default)]
pub id: String,
#[serde(rename = "file", alias = "entrypoint")]
pub path: PathBuf,
pub hash: Option<String>,
#[serde(default)]
pub r#type: String,
#[serde(default)]
pub link: Vec<String>,
#[serde(default)]
pub capabilities: Option<CapabilitiesDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CapabilitiesDef {
#[serde(default)]
pub uplink: bool,
#[serde(default)]
pub net: Vec<String>,
#[serde(default)]
pub kv: Vec<String>,
#[serde(default)]
pub fs_read: Vec<String>,
#[serde(default)]
pub fs_write: Vec<String>,
#[serde(default)]
pub host_process: Vec<String>,
#[serde(default)]
pub net_bind: Vec<String>,
#[serde(default)]
pub ipc_publish: Vec<String>,
#[serde(default)]
pub ipc_subscribe: Vec<String>,
#[serde(default)]
pub identity: Vec<String>,
#[serde(default)]
pub allow_prompt_injection: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvDef {
#[serde(rename = "type")]
pub env_type: String,
pub request: Option<String>,
pub description: Option<String>,
pub default: Option<serde_json::Value>,
#[serde(default)]
pub enum_values: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextFileDef {
pub name: String,
pub file: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandDef {
pub name: String,
pub description: Option<String>,
pub file: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerDef {
pub id: String,
pub description: Option<String>,
#[serde(rename = "type")]
pub server_type: Option<String>,
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillDef {
pub name: String,
pub description: Option<String>,
pub file: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UplinkDef {
pub name: String,
pub platform: String,
pub profile: UplinkProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterceptorDef {
pub event: String,
pub action: String,
#[serde(default = "default_interceptor_priority")]
pub priority: u32,
}
const fn default_interceptor_priority() -> u32 {
100
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TopicDirection {
Publish,
Subscribe,
}
impl fmt::Display for TopicDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Publish => f.write_str("publish"),
Self::Subscribe => f.write_str("subscribe"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopicDef {
pub name: String,
pub direction: TopicDirection,
pub description: Option<String>,
pub schema: Option<PathBuf>,
}