use crate::packs::repository::PackRepository;
use crate::packs::types::Pack;
use async_trait::async_trait;
use ggen_utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{info, warn};
#[async_trait]
pub trait PackRegistry: Send + Sync {
async fn publish(&self, pack: &Pack, metadata: PublishMetadata) -> Result<PublishReceipt>;
async fn unpublish(&self, pack_id: &str, version: &str) -> Result<()>;
async fn search(&self, query: &SearchQuery) -> Result<Vec<Pack>>;
async fn get_versions(&self, pack_id: &str) -> Result<Vec<Version>>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishMetadata {
pub version: String,
pub changelog: String,
pub tags: Vec<String>,
pub documentation_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishReceipt {
pub pack_id: String,
pub version: String,
pub registry_url: String,
pub published_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
pub text: Option<String>,
pub category: Option<String>,
pub tags: Vec<String>,
pub author: Option<String>,
pub production_ready_only: bool,
pub limit: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub version: String,
pub published_at: String,
pub is_latest: bool,
pub downloads: u64,
}
pub struct InMemoryRegistry {
#[allow(dead_code)]
packs: HashMap<String, Vec<Pack>>,
repository: Box<dyn PackRepository>,
}
impl InMemoryRegistry {
pub fn new(repository: Box<dyn PackRepository>) -> Self {
Self {
packs: HashMap::new(),
repository,
}
}
}
#[async_trait]
impl PackRegistry for InMemoryRegistry {
async fn publish(&self, pack: &Pack, metadata: PublishMetadata) -> Result<PublishReceipt> {
info!("Publishing pack '{}' version {}", pack.id, metadata.version);
if pack.id.is_empty() || pack.name.is_empty() {
return Err(Error::new("Pack ID and name are required"));
}
self.repository.save(pack).await?;
Ok(PublishReceipt {
pack_id: pack.id.clone(),
version: metadata.version,
registry_url: format!("https://registry.ggen.io/packs/{}", pack.id),
published_at: chrono::Utc::now().to_rfc3339(),
})
}
async fn unpublish(&self, pack_id: &str, version: &str) -> Result<()> {
info!("Unpublishing pack '{}' version {}", pack_id, version);
warn!("Unpublish functionality not yet fully implemented");
Ok(())
}
async fn search(&self, query: &SearchQuery) -> Result<Vec<Pack>> {
let mut results = self.repository.list(query.category.as_deref()).await?;
if let Some(text) = &query.text {
let text_lower = text.to_lowercase();
results.retain(|p| {
p.name.to_lowercase().contains(&text_lower)
|| p.description.to_lowercase().contains(&text_lower)
});
}
if !query.tags.is_empty() {
results.retain(|p| query.tags.iter().any(|tag| p.tags.contains(tag)));
}
if let Some(author) = &query.author {
results.retain(|p| p.author.as_ref() == Some(author));
}
if query.production_ready_only {
results.retain(|p| p.production_ready);
}
results.truncate(query.limit);
Ok(results)
}
async fn get_versions(&self, pack_id: &str) -> Result<Vec<Version>> {
let pack = self.repository.load(pack_id).await?;
Ok(vec![Version {
version: pack.version,
published_at: chrono::Utc::now().to_rfc3339(),
is_latest: true,
downloads: 0,
}])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::repository::FileSystemRepository;
use crate::packs::types::PackMetadata;
async fn create_test_registry() -> InMemoryRegistry {
let temp_dir = tempfile::tempdir().unwrap();
let repo = FileSystemRepository::new(temp_dir.path());
InMemoryRegistry::new(Box::new(repo))
}
fn create_test_pack(id: &str) -> Pack {
Pack {
id: id.to_string(),
name: format!("Pack {}", id),
version: "1.0.0".to_string(),
description: "Test pack".to_string(),
category: "test".to_string(),
author: Some("Test Author".to_string()),
repository: None,
license: Some("MIT".to_string()),
packages: vec![],
templates: vec![],
sparql_queries: HashMap::new(),
dependencies: vec![],
tags: vec!["test".to_string()],
keywords: vec![],
production_ready: true,
metadata: PackMetadata::default(),
}
}
#[tokio::test]
async fn test_publish_pack() {
let registry = create_test_registry().await;
let pack = create_test_pack("test-pack");
let metadata = PublishMetadata {
version: "1.0.0".to_string(),
changelog: "Initial release".to_string(),
tags: vec!["test".to_string()],
documentation_url: None,
};
let result = registry.publish(&pack, metadata).await;
assert!(result.is_ok());
let receipt = result.unwrap();
assert_eq!(receipt.pack_id, "test-pack");
assert_eq!(receipt.version, "1.0.0");
}
#[tokio::test]
async fn test_search_packs() {
let registry = create_test_registry().await;
let pack = create_test_pack("searchable-pack");
let metadata = PublishMetadata {
version: "1.0.0".to_string(),
changelog: "Initial release".to_string(),
tags: vec!["test".to_string()],
documentation_url: None,
};
registry.publish(&pack, metadata).await.unwrap();
let query = SearchQuery {
text: Some("searchable".to_string()),
category: None,
tags: vec![],
author: None,
production_ready_only: false,
limit: 10,
};
let results = registry.search(&query).await.unwrap();
assert!(!results.is_empty());
}
}