use crate::plugin_manifest::PluginManifest;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MarketplaceError {
#[error("Registry error: {0}")]
RegistryError(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Plugin not found: {0}")]
PluginNotFound(String),
#[error("Download error: {0}")]
DownloadError(String),
#[error("Invalid plugin package: {0}")]
InvalidPackage(String),
#[error("Publication error: {0}")]
PublicationError(String),
#[error("Authentication error: {0}")]
AuthError(String),
#[error("IO error: {0}")]
IoError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchCriteria {
pub query: Option<String>,
pub category: Option<String>,
pub author: Option<String>,
pub keywords: Vec<String>,
pub min_version: Option<String>,
pub limit: usize,
pub offset: usize,
}
impl Default for SearchCriteria {
fn default() -> Self {
Self {
query: None,
category: None,
author: None,
keywords: vec![],
min_version: None,
limit: 20,
offset: 0,
}
}
}
impl SearchCriteria {
pub fn new() -> Self {
Self::default()
}
pub fn with_query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_keyword(mut self, keyword: impl Into<String>) -> Self {
self.keywords.push(keyword.into());
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub manifest: PluginManifest,
pub download_url: String,
pub downloads: u64,
pub rating: f32,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryConfig {
pub url: String,
pub api_key: Option<String>,
pub timeout: Duration,
pub verify_ssl: bool,
}
impl Default for RegistryConfig {
fn default() -> Self {
Self {
url: "https://plugins.oxify.io".to_string(),
api_key: None,
timeout: Duration::from_secs(30),
verify_ssl: true,
}
}
}
impl RegistryConfig {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
..Default::default()
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn without_ssl_verification(mut self) -> Self {
self.verify_ssl = false;
self
}
}
pub struct RegistryClient {
config: RegistryConfig,
client: Client,
}
impl RegistryClient {
pub fn new(config: RegistryConfig) -> Result<Self, MarketplaceError> {
let client = Client::builder()
.timeout(config.timeout)
.danger_accept_invalid_certs(!config.verify_ssl)
.build()
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
Ok(Self { config, client })
}
pub async fn search(
&self,
criteria: SearchCriteria,
) -> Result<Vec<SearchResult>, MarketplaceError> {
let url = format!("{}/api/v1/plugins/search", self.config.url);
let mut request = self.client.get(&url);
if let Some(query) = &criteria.query {
request = request.query(&[("q", query)]);
}
if let Some(category) = &criteria.category {
request = request.query(&[("category", category)]);
}
if let Some(author) = &criteria.author {
request = request.query(&[("author", author)]);
}
if !criteria.keywords.is_empty() {
request = request.query(&[("keywords", criteria.keywords.join(","))]);
}
request = request.query(&[("limit", criteria.limit.to_string())]);
request = request.query(&[("offset", criteria.offset.to_string())]);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(MarketplaceError::RegistryError(format!(
"Registry returned status: {}",
response.status()
)));
}
let results: Vec<SearchResult> = response
.json()
.await
.map_err(|e| MarketplaceError::RegistryError(e.to_string()))?;
Ok(results)
}
pub async fn get_plugin(&self, name: &str) -> Result<SearchResult, MarketplaceError> {
let url = format!("{}/api/v1/plugins/{}", self.config.url, name);
let mut request = self.client.get(&url);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if response.status().as_u16() == 404 {
return Err(MarketplaceError::PluginNotFound(name.to_string()));
}
if !response.status().is_success() {
return Err(MarketplaceError::RegistryError(format!(
"Registry returned status: {}",
response.status()
)));
}
let result: SearchResult = response
.json()
.await
.map_err(|e| MarketplaceError::RegistryError(e.to_string()))?;
Ok(result)
}
pub async fn download(
&self,
name: &str,
destination: &Path,
) -> Result<PathBuf, MarketplaceError> {
let plugin_info = self.get_plugin(name).await?;
let mut request = self.client.get(&plugin_info.download_url);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(MarketplaceError::DownloadError(format!(
"Download failed with status: {}",
response.status()
)));
}
let plugin_path = destination.join(format!("{}.tar.gz", name));
let bytes = response
.bytes()
.await
.map_err(|e| MarketplaceError::DownloadError(e.to_string()))?;
std::fs::create_dir_all(destination)
.map_err(|e| MarketplaceError::IoError(e.to_string()))?;
std::fs::write(&plugin_path, bytes)
.map_err(|e| MarketplaceError::IoError(e.to_string()))?;
Ok(plugin_path)
}
pub async fn publish(
&self,
manifest_path: &Path,
package_path: &Path,
) -> Result<(), MarketplaceError> {
if self.config.api_key.is_none() {
return Err(MarketplaceError::AuthError(
"API key is required for publishing".to_string(),
));
}
let manifest = PluginManifest::from_file(manifest_path)
.map_err(|e| MarketplaceError::InvalidPackage(e.to_string()))?;
manifest
.validate()
.map_err(|e| MarketplaceError::InvalidPackage(e.to_string()))?;
let create_url = format!("{}/api/v1/plugins", self.config.url);
let manifest_json = serde_json::to_string(&manifest)
.map_err(|e| MarketplaceError::InvalidPackage(e.to_string()))?;
let create_request = self
.client
.post(&create_url)
.header(
"Authorization",
format!("Bearer {}", self.config.api_key.as_ref().unwrap()),
)
.header("Content-Type", "application/json")
.body(manifest_json);
let create_response = create_request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if !create_response.status().is_success() {
let error_text = create_response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(MarketplaceError::PublicationError(format!(
"Failed to create plugin entry: {}",
error_text
)));
}
let upload_url = format!(
"{}/api/v1/plugins/{}/upload",
self.config.url, manifest.plugin.name
);
let package_bytes =
std::fs::read(package_path).map_err(|e| MarketplaceError::IoError(e.to_string()))?;
let upload_request = self
.client
.put(&upload_url)
.header(
"Authorization",
format!("Bearer {}", self.config.api_key.as_ref().unwrap()),
)
.header("Content-Type", "application/gzip")
.body(package_bytes);
let upload_response = upload_request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if !upload_response.status().is_success() {
let error_text = upload_response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(MarketplaceError::PublicationError(format!(
"Package upload failed: {}",
error_text
)));
}
Ok(())
}
pub async fn get_versions(&self, name: &str) -> Result<Vec<String>, MarketplaceError> {
let url = format!("{}/api/v1/plugins/{}/versions", self.config.url, name);
let mut request = self.client.get(&url);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.await
.map_err(|e| MarketplaceError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(MarketplaceError::RegistryError(format!(
"Registry returned status: {}",
response.status()
)));
}
let versions: Vec<String> = response
.json()
.await
.map_err(|e| MarketplaceError::RegistryError(e.to_string()))?;
Ok(versions)
}
}
pub struct MarketplaceManager {
registries: HashMap<String, RegistryClient>,
default_registry: String,
}
impl MarketplaceManager {
pub fn new() -> Self {
Self {
registries: HashMap::new(),
default_registry: "default".to_string(),
}
}
pub fn add_registry(
&mut self,
name: impl Into<String>,
config: RegistryConfig,
) -> Result<(), MarketplaceError> {
let name = name.into();
let client = RegistryClient::new(config)?;
self.registries.insert(name, client);
Ok(())
}
pub fn set_default_registry(&mut self, name: impl Into<String>) {
self.default_registry = name.into();
}
pub fn get_registry(&self, name: &str) -> Option<&RegistryClient> {
self.registries.get(name)
}
pub fn default_registry(&self) -> Option<&RegistryClient> {
self.registries.get(&self.default_registry)
}
pub async fn search_all(
&self,
criteria: SearchCriteria,
) -> Result<Vec<SearchResult>, MarketplaceError> {
let mut all_results = Vec::new();
for client in self.registries.values() {
match client.search(criteria.clone()).await {
Ok(mut results) => all_results.append(&mut results),
Err(e) => {
tracing::warn!("Failed to search registry: {}", e);
}
}
}
Ok(all_results)
}
pub async fn search(
&self,
criteria: SearchCriteria,
) -> Result<Vec<SearchResult>, MarketplaceError> {
let client = self.default_registry().ok_or_else(|| {
MarketplaceError::RegistryError("No default registry configured".to_string())
})?;
client.search(criteria).await
}
pub async fn download(
&self,
name: &str,
destination: &Path,
) -> Result<PathBuf, MarketplaceError> {
let client = self.default_registry().ok_or_else(|| {
MarketplaceError::RegistryError("No default registry configured".to_string())
})?;
client.download(name, destination).await
}
pub async fn publish(
&self,
manifest_path: &Path,
package_path: &Path,
) -> Result<(), MarketplaceError> {
let client = self.default_registry().ok_or_else(|| {
MarketplaceError::RegistryError("No default registry configured".to_string())
})?;
client.publish(manifest_path, package_path).await
}
}
impl Default for MarketplaceManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_criteria_default() {
let criteria = SearchCriteria::default();
assert_eq!(criteria.limit, 20);
assert_eq!(criteria.offset, 0);
assert!(criteria.query.is_none());
}
#[test]
fn test_search_criteria_builder() {
let criteria = SearchCriteria::new()
.with_query("test")
.with_category("transform")
.with_author("john")
.with_keyword("ml")
.with_limit(50)
.with_offset(10);
assert_eq!(criteria.query, Some("test".to_string()));
assert_eq!(criteria.category, Some("transform".to_string()));
assert_eq!(criteria.author, Some("john".to_string()));
assert_eq!(criteria.keywords, vec!["ml"]);
assert_eq!(criteria.limit, 50);
assert_eq!(criteria.offset, 10);
}
#[test]
fn test_registry_config_default() {
let config = RegistryConfig::default();
assert_eq!(config.url, "https://plugins.oxify.io");
assert!(config.api_key.is_none());
assert!(config.verify_ssl);
}
#[test]
fn test_registry_config_builder() {
let config = RegistryConfig::new("https://custom.registry.io")
.with_api_key("secret-key")
.with_timeout(Duration::from_secs(60))
.without_ssl_verification();
assert_eq!(config.url, "https://custom.registry.io");
assert_eq!(config.api_key, Some("secret-key".to_string()));
assert_eq!(config.timeout, Duration::from_secs(60));
assert!(!config.verify_ssl);
}
#[test]
fn test_marketplace_manager_creation() {
let manager = MarketplaceManager::new();
assert_eq!(manager.default_registry, "default");
assert_eq!(manager.registries.len(), 0);
}
#[test]
fn test_marketplace_manager_add_registry() {
let mut manager = MarketplaceManager::new();
let config = RegistryConfig::default();
let result = manager.add_registry("test-registry", config);
assert!(result.is_ok());
assert_eq!(manager.registries.len(), 1);
}
#[test]
fn test_marketplace_manager_set_default() {
let mut manager = MarketplaceManager::new();
manager.set_default_registry("custom");
assert_eq!(manager.default_registry, "custom");
}
#[test]
fn test_marketplace_manager_get_registry() {
let mut manager = MarketplaceManager::new();
let config = RegistryConfig::default();
manager.add_registry("test", config).unwrap();
let registry = manager.get_registry("test");
assert!(registry.is_some());
let missing = manager.get_registry("missing");
assert!(missing.is_none());
}
}