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<()> {
166 let payload = serde_json::json!({ "ttl_secs": ttl_secs });
167 self.make_request(
168 "POST",
169 &format!("/collections/{collection}/ttl"),
170 Some(payload),
171 )
172 .await?;
173 Ok(())
174 }
175
176 pub async fn get_collection_ttl(&self, collection: &str) -> Result<Option<u64>> {
181 let response = self
182 .make_request("GET", &format!("/collections/{collection}/ttl"), None)
183 .await?;
184 let parsed: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
185 VectorizerError::server(format!("Failed to parse get_collection_ttl response: {e}"))
186 })?;
187 Ok(parsed.get("ttl_secs").and_then(serde_json::Value::as_u64))
188 }
189
190 pub async fn rename_collection(&self, collection: &str, new_name: &str) -> Result<()> {
200 let payload = serde_json::json!({ "new_name": new_name });
201 self.make_request(
202 "POST",
203 &format!("/collections/{collection}/rename"),
204 Some(payload),
205 )
206 .await?;
207 Ok(())
208 }
209
210 pub async fn reindex_collection(
221 &self,
222 collection: &str,
223 params: crate::models::ReindexParams,
224 ) -> Result<crate::models::ReindexJob> {
225 let payload = serde_json::json!({
226 "m": params.m,
227 "ef_construction": params.ef_construction,
228 "ef_search": params.ef_search,
229 });
230 let response = self
231 .make_request(
232 "POST",
233 &format!("/collections/{collection}/reindex"),
234 Some(payload),
235 )
236 .await?;
237 serde_json::from_str(&response).map_err(|e| {
238 VectorizerError::server(format!("Failed to parse reindex_collection response: {e}"))
239 })
240 }
241
242 pub async fn snapshot_collection_native(
250 &self,
251 collection: &str,
252 ) -> Result<crate::models::NativeSnapshotInfo> {
253 let response = self
254 .make_request(
255 "POST",
256 &format!("/collections/{collection}/snapshot"),
257 Some(serde_json::json!({})),
258 )
259 .await?;
260 serde_json::from_str(&response).map_err(|e| {
261 VectorizerError::server(format!(
262 "Failed to parse snapshot_collection_native response: {e}"
263 ))
264 })
265 }
266
267 pub async fn list_collection_snapshots_native(
273 &self,
274 collection: &str,
275 ) -> Result<Vec<crate::models::NativeSnapshotInfo>> {
276 let response = self
277 .make_request("GET", &format!("/collections/{collection}/snapshots"), None)
278 .await?;
279 let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
280 VectorizerError::server(format!(
281 "Failed to parse list_collection_snapshots_native response: {e}"
282 ))
283 })?;
284 let arr = val
285 .get("snapshots")
286 .and_then(|s| s.as_array())
287 .cloned()
288 .unwrap_or_default();
289 arr.into_iter()
290 .map(|v| {
291 serde_json::from_value(v).map_err(|e| {
292 VectorizerError::server(format!("Failed to parse snapshot entry: {e}"))
293 })
294 })
295 .collect()
296 }
297
298 pub async fn restore_collection_snapshot_native(
304 &self,
305 collection: &str,
306 snapshot_id: &str,
307 ) -> Result<()> {
308 self.make_request(
309 "POST",
310 &format!("/collections/{collection}/snapshots/{snapshot_id}/restore"),
311 Some(serde_json::json!({})),
312 )
313 .await?;
314 Ok(())
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use serde_json::json;
321
322 use crate::models::{NativeSnapshotInfo, ReencodeJob, ReindexJob, ReindexParams};
323
324 #[test]
325 fn reencode_job_wire_shape() {
326 let raw = json!({
328 "job_id": "reencode-myc-1746000000",
329 "collection": "myc",
330 "state": "completed",
331 "target_encoding": "fp32",
332 "progress": 1.0,
333 });
334 let job: ReencodeJob = serde_json::from_value(raw).unwrap();
335 assert_eq!(job.job_id, "reencode-myc-1746000000");
336 assert_eq!(job.state, "completed");
337 assert_eq!(job.target_encoding, "fp32");
338 }
339
340 #[test]
341 fn set_collection_ttl_payload_shape() {
342 let with_ttl = json!({ "ttl_secs": 3600u64 });
344 assert_eq!(with_ttl["ttl_secs"], 3600);
345
346 let clear_ttl = json!({ "ttl_secs": serde_json::Value::Null });
347 assert!(clear_ttl["ttl_secs"].is_null());
348 }
349
350 #[test]
351 fn get_collection_ttl_reads_both_wire_shapes() {
352 let configured = json!({"collection": "myc", "ttl_secs": 900u64});
354 assert_eq!(
355 configured
356 .get("ttl_secs")
357 .and_then(serde_json::Value::as_u64),
358 Some(900)
359 );
360
361 let cleared = json!({"collection": "myc", "ttl_secs": serde_json::Value::Null});
362 assert_eq!(
363 cleared.get("ttl_secs").and_then(serde_json::Value::as_u64),
364 None
365 );
366 }
367
368 #[test]
371 fn rename_collection_payload_shape() {
372 let payload = json!({ "new_name": "docs_v2" });
374 assert_eq!(payload["new_name"], "docs_v2");
375 }
376
377 #[test]
378 fn reindex_params_serialize() {
379 let params = ReindexParams {
380 m: 32,
381 ef_construction: 400,
382 ef_search: 200,
383 };
384 let v = serde_json::to_value(¶ms).unwrap();
385 assert_eq!(v["m"], 32);
386 assert_eq!(v["ef_construction"], 400);
387 assert_eq!(v["ef_search"], 200);
388 }
389
390 #[test]
391 fn reindex_job_wire_shape() {
392 let raw = json!({
394 "job_id": "reindex-docs-1746000001",
395 "collection": "docs",
396 "state": "completed",
397 "params": { "m": 32, "ef_construction": 400, "ef_search": 200 },
398 "progress": 1.0,
399 });
400 let job: ReindexJob = serde_json::from_value(raw).unwrap();
401 assert_eq!(job.job_id, "reindex-docs-1746000001");
402 assert_eq!(job.state, "completed");
403 assert!((job.progress - 1.0).abs() < f64::EPSILON);
404 }
405
406 #[test]
407 fn native_snapshot_info_wire_shape() {
408 let raw = json!({
410 "id": "snap-abc-123",
411 "collection": "docs",
412 "created_at": "2026-05-02T00:00:00Z",
413 "size_bytes": 4096u64,
414 "status": "ok",
415 });
416 let info: NativeSnapshotInfo = serde_json::from_value(raw).unwrap();
417 assert_eq!(info.id, "snap-abc-123");
418 assert_eq!(info.collection, "docs");
419 assert_eq!(info.size_bytes, 4096);
420 }
421
422 #[test]
423 fn list_snapshots_response_parses() {
424 let raw = json!({
426 "collection": "docs",
427 "snapshots": [
428 {
429 "id": "snap-abc-123",
430 "collection": "docs",
431 "created_at": "2026-05-02T00:00:00Z",
432 "size_bytes": 4096u64,
433 }
434 ],
435 "total": 1,
436 });
437 let arr = raw["snapshots"].as_array().unwrap();
438 let snaps: Vec<NativeSnapshotInfo> = arr
439 .iter()
440 .map(|v| serde_json::from_value(v.clone()).unwrap())
441 .collect();
442 assert_eq!(snaps.len(), 1);
443 assert_eq!(snaps[0].id, "snap-abc-123");
444 }
445
446 #[test]
447 fn restore_snapshot_payload_shape() {
448 let payload = json!({});
450 assert!(payload.as_object().map(|o| o.is_empty()).unwrap_or(false));
451 }
452}