1use super::VectorizerClient;
8use crate::error::{Result, VectorizerError};
9use crate::models::*;
10
11impl VectorizerClient {
12 pub async fn list_collections(&self) -> Result<Vec<Collection>> {
16 let response = self.make_request("GET", "/collections", None).await?;
17 let collections: Vec<Collection> = if let Ok(wrapper) =
18 serde_json::from_str::<serde_json::Value>(&response)
19 {
20 if let Some(arr) = wrapper.get("collections").and_then(|v| v.as_array()) {
21 serde_json::from_value(serde_json::Value::Array(arr.clone())).map_err(|e| {
22 VectorizerError::server(format!("Failed to parse collections array: {e}"))
23 })?
24 } else if wrapper.is_array() {
25 serde_json::from_value(wrapper).map_err(|e| {
26 VectorizerError::server(format!("Failed to parse collections response: {e}"))
27 })?
28 } else {
29 return Err(VectorizerError::server(
30 "Unexpected collections response format".to_string(),
31 ));
32 }
33 } else {
34 return Err(VectorizerError::server(
35 "Failed to parse collections response".to_string(),
36 ));
37 };
38 Ok(collections)
39 }
40
41 pub async fn create_collection(
46 &self,
47 name: &str,
48 dimension: usize,
49 metric: Option<SimilarityMetric>,
50 ) -> Result<CollectionInfo> {
51 let mut payload = serde_json::Map::new();
52 payload.insert(
53 "name".to_string(),
54 serde_json::Value::String(name.to_string()),
55 );
56 payload.insert(
57 "dimension".to_string(),
58 serde_json::Value::Number(dimension.into()),
59 );
60 payload.insert(
61 "metric".to_string(),
62 serde_json::Value::String(format!("{:?}", metric.unwrap_or_default()).to_lowercase()),
63 );
64
65 let response = self
66 .make_request(
67 "POST",
68 "/collections",
69 Some(serde_json::Value::Object(payload)),
70 )
71 .await?;
72 let create_response: CreateCollectionResponse =
73 serde_json::from_str(&response).map_err(|e| {
74 VectorizerError::server(format!("Failed to parse create collection response: {e}"))
75 })?;
76
77 let info = CollectionInfo {
78 name: create_response.collection,
79 dimension,
80 metric: format!("{:?}", metric.unwrap_or_default()).to_lowercase(),
81 vector_count: 0,
82 document_count: 0,
83 created_at: String::new(),
84 updated_at: String::new(),
85 indexing_status: Some(crate::models::IndexingStatus {
86 status: "created".to_string(),
87 progress: 0.0,
88 total_documents: 0,
89 processed_documents: 0,
90 vector_count: 0,
91 estimated_time_remaining: None,
92 last_updated: String::new(),
93 }),
94 size: None,
95 quantization: None,
96 normalization: None,
97 status: Some("created".to_string()),
98 };
99 Ok(info)
100 }
101
102 pub async fn delete_collection(&self, name: &str) -> Result<()> {
104 self.make_request("DELETE", &format!("/collections/{name}"), None)
105 .await?;
106 Ok(())
107 }
108
109 pub async fn get_collection_info(&self, collection: &str) -> Result<CollectionInfo> {
112 let response = self
113 .make_request("GET", &format!("/collections/{collection}"), None)
114 .await?;
115 let info: CollectionInfo = serde_json::from_str(&response).map_err(|e| {
116 VectorizerError::server(format!("Failed to parse collection info: {e}"))
117 })?;
118 Ok(info)
119 }
120
121 pub async fn reencode_collection(
132 &self,
133 collection: &str,
134 target_encoding: &str,
135 ) -> Result<ReencodeJob> {
136 let payload = serde_json::json!({ "target_encoding": target_encoding });
137 let response = self
138 .make_request(
139 "POST",
140 &format!("/collections/{collection}/reencode"),
141 Some(payload),
142 )
143 .await?;
144 serde_json::from_str(&response).map_err(|e| {
145 VectorizerError::server(format!("Failed to parse reencode_collection response: {e}"))
146 })
147 }
148
149 pub async fn set_collection_ttl(&self, collection: &str, ttl_secs: Option<u64>) -> Result<()> {
158 let payload = serde_json::json!({ "ttl_secs": ttl_secs });
159 self.make_request(
160 "POST",
161 &format!("/collections/{collection}/ttl"),
162 Some(payload),
163 )
164 .await?;
165 Ok(())
166 }
167
168 pub async fn rename_collection(&self, collection: &str, new_name: &str) -> Result<()> {
178 let payload = serde_json::json!({ "new_name": new_name });
179 self.make_request(
180 "POST",
181 &format!("/collections/{collection}/rename"),
182 Some(payload),
183 )
184 .await?;
185 Ok(())
186 }
187
188 pub async fn reindex_collection(
199 &self,
200 collection: &str,
201 params: crate::models::ReindexParams,
202 ) -> Result<crate::models::ReindexJob> {
203 let payload = serde_json::json!({
204 "m": params.m,
205 "ef_construction": params.ef_construction,
206 "ef_search": params.ef_search,
207 });
208 let response = self
209 .make_request(
210 "POST",
211 &format!("/collections/{collection}/reindex"),
212 Some(payload),
213 )
214 .await?;
215 serde_json::from_str(&response).map_err(|e| {
216 VectorizerError::server(format!("Failed to parse reindex_collection response: {e}"))
217 })
218 }
219
220 pub async fn snapshot_collection_native(
228 &self,
229 collection: &str,
230 ) -> Result<crate::models::NativeSnapshotInfo> {
231 let response = self
232 .make_request(
233 "POST",
234 &format!("/collections/{collection}/snapshot"),
235 Some(serde_json::json!({})),
236 )
237 .await?;
238 serde_json::from_str(&response).map_err(|e| {
239 VectorizerError::server(format!(
240 "Failed to parse snapshot_collection_native response: {e}"
241 ))
242 })
243 }
244
245 pub async fn list_collection_snapshots_native(
251 &self,
252 collection: &str,
253 ) -> Result<Vec<crate::models::NativeSnapshotInfo>> {
254 let response = self
255 .make_request("GET", &format!("/collections/{collection}/snapshots"), None)
256 .await?;
257 let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
258 VectorizerError::server(format!(
259 "Failed to parse list_collection_snapshots_native response: {e}"
260 ))
261 })?;
262 let arr = val
263 .get("snapshots")
264 .and_then(|s| s.as_array())
265 .cloned()
266 .unwrap_or_default();
267 arr.into_iter()
268 .map(|v| {
269 serde_json::from_value(v).map_err(|e| {
270 VectorizerError::server(format!("Failed to parse snapshot entry: {e}"))
271 })
272 })
273 .collect()
274 }
275
276 pub async fn restore_collection_snapshot_native(
282 &self,
283 collection: &str,
284 snapshot_id: &str,
285 ) -> Result<()> {
286 self.make_request(
287 "POST",
288 &format!("/collections/{collection}/snapshots/{snapshot_id}/restore"),
289 Some(serde_json::json!({})),
290 )
291 .await?;
292 Ok(())
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use serde_json::json;
299
300 use crate::models::{NativeSnapshotInfo, ReencodeJob, ReindexJob, ReindexParams};
301
302 #[test]
303 fn reencode_job_wire_shape() {
304 let raw = json!({
306 "job_id": "reencode-myc-1746000000",
307 "collection": "myc",
308 "state": "completed",
309 "target_encoding": "fp32",
310 "progress": 1.0,
311 });
312 let job: ReencodeJob = serde_json::from_value(raw).unwrap();
313 assert_eq!(job.job_id, "reencode-myc-1746000000");
314 assert_eq!(job.state, "completed");
315 assert_eq!(job.target_encoding, "fp32");
316 }
317
318 #[test]
319 fn set_collection_ttl_payload_shape() {
320 let with_ttl = json!({ "ttl_secs": 3600u64 });
322 assert_eq!(with_ttl["ttl_secs"], 3600);
323
324 let clear_ttl = json!({ "ttl_secs": serde_json::Value::Null });
325 assert!(clear_ttl["ttl_secs"].is_null());
326 }
327
328 #[test]
331 fn rename_collection_payload_shape() {
332 let payload = json!({ "new_name": "docs_v2" });
334 assert_eq!(payload["new_name"], "docs_v2");
335 }
336
337 #[test]
338 fn reindex_params_serialize() {
339 let params = ReindexParams {
340 m: 32,
341 ef_construction: 400,
342 ef_search: 200,
343 };
344 let v = serde_json::to_value(¶ms).unwrap();
345 assert_eq!(v["m"], 32);
346 assert_eq!(v["ef_construction"], 400);
347 assert_eq!(v["ef_search"], 200);
348 }
349
350 #[test]
351 fn reindex_job_wire_shape() {
352 let raw = json!({
354 "job_id": "reindex-docs-1746000001",
355 "collection": "docs",
356 "state": "completed",
357 "params": { "m": 32, "ef_construction": 400, "ef_search": 200 },
358 "progress": 1.0,
359 });
360 let job: ReindexJob = serde_json::from_value(raw).unwrap();
361 assert_eq!(job.job_id, "reindex-docs-1746000001");
362 assert_eq!(job.state, "completed");
363 assert!((job.progress - 1.0).abs() < f64::EPSILON);
364 }
365
366 #[test]
367 fn native_snapshot_info_wire_shape() {
368 let raw = json!({
370 "id": "snap-abc-123",
371 "collection": "docs",
372 "created_at": "2026-05-02T00:00:00Z",
373 "size_bytes": 4096u64,
374 "status": "ok",
375 });
376 let info: NativeSnapshotInfo = serde_json::from_value(raw).unwrap();
377 assert_eq!(info.id, "snap-abc-123");
378 assert_eq!(info.collection, "docs");
379 assert_eq!(info.size_bytes, 4096);
380 }
381
382 #[test]
383 fn list_snapshots_response_parses() {
384 let raw = json!({
386 "collection": "docs",
387 "snapshots": [
388 {
389 "id": "snap-abc-123",
390 "collection": "docs",
391 "created_at": "2026-05-02T00:00:00Z",
392 "size_bytes": 4096u64,
393 }
394 ],
395 "total": 1,
396 });
397 let arr = raw["snapshots"].as_array().unwrap();
398 let snaps: Vec<NativeSnapshotInfo> = arr
399 .iter()
400 .map(|v| serde_json::from_value(v.clone()).unwrap())
401 .collect();
402 assert_eq!(snaps.len(), 1);
403 assert_eq!(snaps[0].id, "snap-abc-123");
404 }
405
406 #[test]
407 fn restore_snapshot_payload_shape() {
408 let payload = json!({});
410 assert!(payload.as_object().map(|o| o.is_empty()).unwrap_or(false));
411 }
412}