//! Registry client for fetching tool manifests and binaries over HTTPS. use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Error)] pub enum RegistryError { #[error("http error: {0}")] Http(#[from] reqwest::Error), #[error("tool not found: {0}")] ToolNotFound(String), #[error("invalid registry response: {0}")] InvalidResponse(String), #[error("io error: {0}")] Io(#[from] std::io::Error), } /// An entry in the registry index. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistryEntry { pub name: String, pub version: String, pub description: String, pub hash: String, pub size: u64, #[serde(default)] pub categories: Vec, } /// The full registry index. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistryIndex { pub registry: String, pub tools: Vec, } /// A configured registry endpoint. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Registry { pub url: String, } /// Client for interacting with WasmBox registries. pub struct RegistryClient { http: reqwest::Client, registries: Vec, } impl RegistryClient { pub fn new(registries: Vec) -> Self { let http = reqwest::Client::builder() .user_agent(format!("wasmbox/{}", env!("CARGO_PKG_VERSION"))) .build() .expect("failed to create HTTP client"); Self { http, registries } } /// Fetch the index from a registry. pub async fn fetch_index(&self, registry: &Registry) -> Result { let url = format!("{}/index.json", registry.url.trim_end_matches('/')); let resp = self.http.get(&url).send().await?.error_for_status()?; let index: RegistryIndex = resp.json().await?; Ok(index) } /// Search all registries for tools matching a query. pub async fn search(&self, query: &str) -> Result, RegistryError> { let mut results = Vec::new(); let query_lower = query.to_lowercase(); for registry in &self.registries { match self.fetch_index(registry).await { Ok(index) => { for tool in index.tools { if tool.name.to_lowercase().contains(&query_lower) || tool.description.to_lowercase().contains(&query_lower) || tool .categories .iter() .any(|c| c.to_lowercase().contains(&query_lower)) { results.push(tool); } } } Err(e) => { eprintln!("warning: failed to fetch from {}: {e}", registry.url); } } } Ok(results) } /// Fetch a tool's manifest JSON from a registry. pub async fn fetch_manifest( &self, registry: &Registry, tool_name: &str, ) -> Result { let url = format!( "{}/tools/{}.json", registry.url.trim_end_matches('/'), tool_name, ); let resp = self.http.get(&url).send().await?; if resp.status() == reqwest::StatusCode::NOT_FOUND { return Err(RegistryError::ToolNotFound(tool_name.to_string())); } let text = resp.error_for_status()?.text().await?; Ok(text) } /// Download a tool's .wasm binary from a registry. pub async fn fetch_binary( &self, registry: &Registry, tool_name: &str, ) -> Result, RegistryError> { let url = format!( "{}/tools/{}.wasm", registry.url.trim_end_matches('/'), tool_name, ); let resp = self.http.get(&url).send().await?; if resp.status() == reqwest::StatusCode::NOT_FOUND { return Err(RegistryError::ToolNotFound(tool_name.to_string())); } let bytes = resp.error_for_status()?.bytes().await?; Ok(bytes.to_vec()) } /// Get the list of configured registries. pub fn registries(&self) -> &[Registry] { &self.registries } } #[cfg(test)] mod tests { use super::*; #[test] fn create_client() { let client = RegistryClient::new(vec![Registry { url: "https://registry.example.com".to_string(), }]); assert_eq!(client.registries().len(), 1); } }