1use crate::api::types::*;
2use crate::config::Config;
3use crate::utils::error::{CarpError, CarpResult};
4use reqwest::{Client, ClientBuilder, Response};
5use std::time::Duration;
6
7pub struct ApiClient {
9 client: Client,
10 base_url: String,
11 api_token: Option<String>,
12}
13
14impl ApiClient {
15 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 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(¶ms)
48 .send()
49 .await?;
50
51 self.handle_response(response).await
52 }
53
54 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 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 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 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 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 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 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}