use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct RegistryClient {
base_url: String,
api_key: Option<String>,
client: reqwest::Client,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryPackage {
pub id: Uuid,
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub download_count: u64,
pub checksum: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub license: String,
pub repository: Option<String>,
pub homepage: Option<String>,
pub keywords: Vec<String>,
pub dependencies: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
pub query: String,
pub category: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResults {
pub packages: Vec<RegistryPackage>,
pub total: usize,
pub has_more: bool,
}
impl RegistryClient {
pub fn new(base_url: String) -> Self {
Self {
base_url,
api_key: None,
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to build HTTP client"),
}
}
pub fn with_api_key(mut self, api_key: String) -> Self {
self.api_key = Some(api_key);
self
}
pub async fn search(&self, query: &SearchQuery) -> Result<SearchResults, RegistryError> {
#[cfg(feature = "enterprise")]
{
let url = format!("{}/api/search", self.base_url);
let response = self.client.get(&url).query(&query).send().await?;
if !response.status().is_success() {
return Err(RegistryError::ApiError(response.status().as_u16()));
}
let results = response.json::<SearchResults>().await?;
Ok(results)
}
#[cfg(not(feature = "enterprise"))]
{
Err(RegistryError::FeatureNotAvailable(
"Registry search requires enterprise features. Use --features enterprise to enable.".to_string()
))
}
}
pub async fn get_package(
&self,
name: &str,
version: &str,
) -> Result<RegistryPackage, RegistryError> {
#[cfg(feature = "enterprise")]
{
let url = format!("{}/api/packages/{}/{}", self.base_url, name, version);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
return Err(RegistryError::ApiError(response.status().as_u16()));
}
let package = response.json::<RegistryPackage>().await?;
Ok(package)
}
#[cfg(not(feature = "enterprise"))]
{
let _ = (name, version); Err(RegistryError::FeatureNotAvailable(
"package_download".to_string(),
))
}
}
pub async fn download_package(
&self,
name: &str,
version: &str,
) -> Result<Vec<u8>, RegistryError> {
#[cfg(feature = "enterprise")]
{
let url = format!(
"{}/api/packages/{}/{}/download",
self.base_url, name, version
);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
return Err(RegistryError::ApiError(response.status().as_u16()));
}
let bytes = response.bytes().await?.to_vec();
Ok(bytes)
}
#[cfg(not(feature = "enterprise"))]
{
let _ = (name, version); Err(RegistryError::FeatureNotAvailable(
"package_download".to_string(),
))
}
}
pub async fn publish_package(
&self,
metadata: &PackageMetadata,
package_data: Vec<u8>,
) -> Result<RegistryPackage, RegistryError> {
#[cfg(feature = "enterprise")]
{
let api_key = self
.api_key
.as_ref()
.ok_or_else(|| RegistryError::AuthenticationRequired)?;
let url = format!("{}/api/packages", self.base_url);
let form = reqwest::multipart::Form::new()
.text("metadata", serde_json::to_string(metadata)?)
.part("package", reqwest::multipart::Part::bytes(package_data));
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(RegistryError::ApiError(response.status().as_u16()));
}
let package = response.json::<RegistryPackage>().await?;
Ok(package)
}
#[cfg(not(feature = "enterprise"))]
{
let _ = (metadata, package_data); Err(RegistryError::FeatureNotAvailable(
"package_publish".to_string(),
))
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("Network error: {0}")]
NetworkError(reqwest::Error),
#[error("API error: HTTP {0}")]
ApiError(u16),
#[error("Parse error: {0}")]
ParseError(reqwest::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Feature '{0}' is not available in community edition")]
FeatureNotAvailable(String),
#[error("Authentication required for this operation")]
AuthenticationRequired,
#[error("Package not found: {name}@{version}")]
PackageNotFound { name: String, version: String },
#[error("Invalid package format")]
InvalidPackageFormat,
}
impl From<reqwest::Error> for RegistryError {
fn from(err: reqwest::Error) -> Self {
if err.is_decode() {
RegistryError::ParseError(err)
} else {
RegistryError::NetworkError(err)
}
}
}
pub fn default_registry_url() -> String {
std::env::var("RUSTCHAIN_REGISTRY_URL")
.unwrap_or_else(|_| "https://registry.rustchain.ai".to_string())
}
pub fn create_registry_client() -> RegistryClient {
RegistryClient::new(default_registry_url())
}
pub fn create_authenticated_client(api_key: String) -> RegistryClient {
RegistryClient::new(default_registry_url()).with_api_key(api_key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_client_creation() {
let client = RegistryClient::new("https://test.example.com".to_string());
assert_eq!(client.base_url, "https://test.example.com");
assert!(client.api_key.is_none());
}
#[test]
fn test_registry_client_with_api_key() {
let client = RegistryClient::new("https://test.example.com".to_string())
.with_api_key("test-key".to_string());
assert_eq!(client.api_key, Some("test-key".to_string()));
}
#[test]
fn test_default_registry_url() {
let url = default_registry_url();
assert!(!url.is_empty());
assert!(url.starts_with("http"));
}
#[tokio::test]
async fn test_search_behavior() {
let client = create_registry_client();
let query = SearchQuery {
query: "test".to_string(),
category: None,
limit: Some(10),
offset: Some(0),
};
let results = client.search(&query).await;
#[cfg(feature = "enterprise")]
{
match results {
Ok(search_results) => {
let _ = search_results.packages.len();
let _ = search_results.total;
}
Err(_) => {
}
}
}
#[cfg(not(feature = "enterprise"))]
{
let search_results = results.unwrap();
assert_eq!(search_results.packages.len(), 0);
assert_eq!(search_results.total, 0);
assert!(!search_results.has_more);
}
}
#[tokio::test]
async fn test_get_package_behavior() {
let client = create_registry_client();
let result = client.get_package("test-package", "1.0.0").await;
#[cfg(feature = "enterprise")]
{
match result {
Ok(_) => {
}
Err(RegistryError::NetworkError(_)) => {
}
Err(RegistryError::ApiError(_)) => {
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[cfg(not(feature = "enterprise"))]
{
match result {
Err(RegistryError::FeatureNotAvailable(_)) => {
}
_ => panic!("Expected FeatureNotAvailable error"),
}
}
}
#[tokio::test]
async fn test_download_package_behavior() {
let client = create_registry_client();
let result = client.download_package("test-package", "1.0.0").await;
#[cfg(feature = "enterprise")]
{
match result {
Ok(_) => {
}
Err(RegistryError::NetworkError(_)) => {
}
Err(RegistryError::ApiError(_)) => {
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[cfg(not(feature = "enterprise"))]
{
match result {
Err(RegistryError::FeatureNotAvailable(_)) => {
}
_ => panic!("Expected FeatureNotAvailable error"),
}
}
}
}