carp_cli/api/
client.rs

1use crate::api::types::*;
2use crate::config::Config;
3use crate::utils::error::{CarpError, CarpResult};
4use reqwest::{Client, ClientBuilder, Response};
5use std::time::Duration;
6
7/// HTTP client for interacting with the Carp registry API
8pub struct ApiClient {
9    client: Client,
10    base_url: String,
11    api_token: Option<String>,
12}
13
14impl ApiClient {
15    /// Create a new API client from configuration
16    pub fn new(config: &Config) -> CarpResult<Self> {
17        let client = ClientBuilder::new()
18            .timeout(Duration::from_secs(config.timeout))
19            .user_agent(format!("carp-cli/{}", env!("CARGO_PKG_VERSION")))
20            .danger_accept_invalid_certs(!config.verify_ssl)
21            .build()?;
22
23        Ok(Self {
24            client,
25            base_url: config.registry_url.clone(),
26            api_token: config.api_token.clone(),
27        })
28    }
29
30    /// Search for agents in the registry
31    pub async fn search(&self, query: &str, limit: Option<usize>, exact: bool) -> CarpResult<SearchResponse> {
32        let url = format!("{}/api/v1/agents/search", self.base_url);
33        let mut params = vec![("q", query)];
34        
35        let limit_str;
36        if let Some(limit) = limit {
37            limit_str = limit.to_string();
38            params.push(("limit", &limit_str));
39        }
40        
41        if exact {
42            params.push(("exact", "true"));
43        }
44
45        let response = self.client
46            .get(&url)
47            .query(&params)
48            .send()
49            .await?;
50
51        self.handle_response(response).await
52    }
53
54    /// Get download information for a specific agent
55    pub async fn get_agent_download(&self, name: &str, version: Option<&str>) -> CarpResult<AgentDownload> {
56        let version = version.unwrap_or("latest");
57        let url = format!("{}/api/v1/agents/{}/{}/download", self.base_url, name, version);
58
59        let response = self.client
60            .get(&url)
61            .send()
62            .await?;
63
64        self.handle_response(response).await
65    }
66
67    /// Download agent content
68    pub async fn download_agent(&self, download_url: &str) -> CarpResult<bytes::Bytes> {
69        let response = self.client
70            .get(download_url)
71            .send()
72            .await?;
73
74        if !response.status().is_success() {
75            return Err(CarpError::Api {
76                status: response.status().as_u16(),
77                message: "Failed to download agent".to_string(),
78            });
79        }
80
81        let bytes = response.bytes().await?;
82        Ok(bytes)
83    }
84
85    /// Publish an agent to the registry
86    pub async fn publish(&self, request: PublishRequest, content: Vec<u8>) -> CarpResult<PublishResponse> {
87        let token = self.api_token.as_ref()
88            .ok_or_else(|| CarpError::Auth("No API token configured. Please login first.".to_string()))?;
89
90        let url = format!("{}/api/v1/agents/publish", self.base_url);
91
92        // Create multipart form with metadata and content
93        let form = reqwest::multipart::Form::new()
94            .text("metadata", serde_json::to_string(&request)?)
95            .part("content", reqwest::multipart::Part::bytes(content)
96                .file_name("agent.zip")
97                .mime_str("application/zip")?);
98
99        let response = self.client
100            .post(&url)
101            .header("Authorization", format!("Bearer {}", token))
102            .multipart(form)
103            .send()
104            .await?;
105
106        self.handle_response(response).await
107    }
108
109    /// Authenticate with the registry
110    pub async fn authenticate(&self, username: &str, password: &str) -> CarpResult<AuthResponse> {
111        let url = format!("{}/api/v1/auth/login", self.base_url);
112        let request = AuthRequest {
113            username: username.to_string(),
114            password: password.to_string(),
115        };
116
117        let response = self.client
118            .post(&url)
119            .json(&request)
120            .send()
121            .await?;
122
123        self.handle_response(response).await
124    }
125
126    /// Handle API response, parsing JSON or error
127    async fn handle_response<T>(&self, response: Response) -> CarpResult<T>
128    where
129        T: serde::de::DeserializeOwned,
130    {
131        let status = response.status();
132        let text = response.text().await?;
133
134        if status.is_success() {
135            serde_json::from_str(&text).map_err(CarpError::from)
136        } else {
137            // Try to parse as API error, fallback to generic error
138            match serde_json::from_str::<ApiError>(&text) {
139                Ok(api_error) => Err(CarpError::Api {
140                    status: status.as_u16(),
141                    message: api_error.message,
142                }),
143                Err(_) => Err(CarpError::Api {
144                    status: status.as_u16(),
145                    message: text,
146                }),
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use mockito::{mock, server_url};
156
157    #[tokio::test]
158    async fn test_search_request() {
159        let config = Config {
160            registry_url: server_url(),
161            api_token: None,
162            timeout: 30,
163            verify_ssl: true,
164            default_output_dir: None,
165        };
166
167        let _m = mock("GET", "/api/v1/agents/search")
168            .with_status(200)
169            .with_header("content-type", "application/json")
170            .with_body(r#"{"agents": [], "total": 0, "page": 1, "per_page": 10}"#)
171            .create();
172
173        let client = ApiClient::new(&config).unwrap();
174        let result = client.search("test", Some(10), false).await;
175        
176        assert!(result.is_ok());
177        let response = result.unwrap();
178        assert_eq!(response.agents.len(), 0);
179        assert_eq!(response.total, 0);
180    }
181}