1use std::path::{Path, PathBuf};
4
5use clap::Subcommand;
6
7use super::basic::load_dataset;
8use crate::{
9 backend::LocalBackend,
10 registry::{DatasetMetadata, Registry},
11 Dataset,
12};
13
14#[derive(Subcommand, Debug)]
16pub enum RegistryCommands {
17 Init {
19 #[arg(short, long, default_value = ".alimentar")]
21 path: PathBuf,
22 },
23 List {
25 #[arg(short, long, default_value = ".alimentar")]
27 path: PathBuf,
28 },
29 #[command(disable_version_flag = true)]
35 Push {
36 input: PathBuf,
38 #[arg(short, long)]
40 name: String,
41 #[arg(long, default_value = "1.0.0")]
46 version: String,
47 #[arg(short, long, default_value = "")]
49 description: String,
50 #[arg(short, long, default_value = "")]
52 license: String,
53 #[arg(short, long, default_value = "")]
55 tags: String,
56 #[arg(long, default_value = ".alimentar")]
58 registry: PathBuf,
59 },
60 #[command(disable_version_flag = true)]
66 Pull {
67 name: String,
69 #[arg(short, long)]
71 output: PathBuf,
72 #[arg(long)]
77 version: Option<String>,
78 #[arg(long, default_value = ".alimentar")]
80 registry: PathBuf,
81 },
82 Search {
84 query: String,
86 #[arg(short, long, default_value = ".alimentar")]
88 path: PathBuf,
89 },
90 ShowInfo {
92 name: String,
94 #[arg(short, long, default_value = ".alimentar")]
96 path: PathBuf,
97 },
98 #[command(disable_version_flag = true)]
104 Delete {
105 name: String,
107 #[arg(long)]
112 version: String,
113 #[arg(short, long, default_value = ".alimentar")]
115 path: PathBuf,
116 },
117}
118
119pub(crate) fn create_registry(path: &Path) -> crate::Result<Registry> {
121 if !path.exists() {
123 std::fs::create_dir_all(path).map_err(|e| crate::Error::io(e, path))?;
124 }
125 let backend = LocalBackend::new(path)?;
126 Ok(Registry::new(Box::new(backend)))
127}
128
129pub(crate) fn cmd_registry_init(path: &Path) -> crate::Result<()> {
131 let registry = create_registry(path)?;
132 registry.init()?;
133 println!("Initialized registry at: {}", path.display());
134 Ok(())
135}
136
137pub(crate) fn cmd_registry_list(path: &Path) -> crate::Result<()> {
139 let registry = create_registry(path)?;
140 let datasets = registry.list()?;
141
142 if datasets.is_empty() {
143 println!("No datasets in registry.");
144 return Ok(());
145 }
146
147 println!("Datasets in registry:\n");
148 println!(
149 "{:<25} {:<12} {:<10} {:<15} DESCRIPTION",
150 "NAME", "LATEST", "VERSIONS", "ROWS"
151 );
152 println!("{}", "-".repeat(80));
153
154 for ds in datasets {
155 let desc = if ds.metadata.description.len() > 30 {
156 format!("{}...", &ds.metadata.description[..27])
157 } else {
158 ds.metadata.description.clone()
159 };
160 println!(
161 "{:<25} {:<12} {:<10} {:<15} {}",
162 ds.name,
163 ds.latest,
164 ds.versions.len(),
165 ds.num_rows,
166 desc
167 );
168 }
169
170 Ok(())
171}
172
173#[allow(clippy::too_many_arguments)]
175pub(crate) fn cmd_registry_push(
176 input: &Path,
177 name: &str,
178 version: &str,
179 description: &str,
180 license: &str,
181 tags: &str,
182 registry_path: &Path,
183) -> crate::Result<()> {
184 let registry = create_registry(registry_path)?;
185
186 registry.init()?;
188
189 let dataset = load_dataset(input)?;
191
192 let tag_list: Vec<String> = if tags.is_empty() {
194 Vec::new()
195 } else {
196 tags.split(',').map(|s| s.trim().to_string()).collect()
197 };
198
199 let metadata = DatasetMetadata {
201 description: description.to_string(),
202 license: license.to_string(),
203 tags: tag_list,
204 source: Some(input.display().to_string()),
205 citation: None,
206 sha256: None, };
208
209 registry.publish(name, version, &dataset, metadata)?;
211
212 println!(
213 "Published {}@{} ({} rows) to registry",
214 name,
215 version,
216 dataset.len()
217 );
218
219 Ok(())
220}
221
222pub(crate) fn cmd_registry_pull(
224 name: &str,
225 output: &Path,
226 version: Option<&str>,
227 registry_path: &Path,
228) -> crate::Result<()> {
229 let registry = create_registry(registry_path)?;
230
231 let dataset = registry.pull(name, version)?;
233
234 dataset.to_parquet(output)?;
236
237 let ver = version.unwrap_or("latest");
238 println!(
239 "Pulled {}@{} ({} rows) to {}",
240 name,
241 ver,
242 dataset.len(),
243 output.display()
244 );
245
246 Ok(())
247}
248
249pub(crate) fn cmd_registry_search(query: &str, path: &Path) -> crate::Result<()> {
251 let registry = create_registry(path)?;
252 let results = registry.search(query)?;
253
254 if results.is_empty() {
255 println!("No datasets found matching '{}'", query);
256 return Ok(());
257 }
258
259 println!("Search results for '{}':\n", query);
260 println!("{:<25} {:<12} {:<10} DESCRIPTION", "NAME", "LATEST", "ROWS");
261 println!("{}", "-".repeat(70));
262
263 for ds in results {
264 let desc = if ds.metadata.description.len() > 30 {
265 format!("{}...", &ds.metadata.description[..27])
266 } else {
267 ds.metadata.description.clone()
268 };
269 println!(
270 "{:<25} {:<12} {:<10} {}",
271 ds.name, ds.latest, ds.num_rows, desc
272 );
273 }
274
275 Ok(())
276}
277
278pub(crate) fn cmd_registry_show_info(name: &str, path: &Path) -> crate::Result<()> {
280 let registry = create_registry(path)?;
281 let info = registry.get_info(name)?;
282
283 println!("Dataset: {}", info.name);
284 println!("Latest: {}", info.latest);
285 println!("Versions: {}", info.versions.join(", "));
286 println!("Rows: {}", info.num_rows);
287 println!("Size: {} bytes", info.size_bytes);
288 println!();
289 println!("Description: {}", info.metadata.description);
290 println!("License: {}", info.metadata.license);
291 println!("Tags: {}", info.metadata.tags.join(", "));
292
293 if let Some(source) = &info.metadata.source {
294 println!("Source: {}", source);
295 }
296 if let Some(citation) = &info.metadata.citation {
297 println!("Citation: {}", citation);
298 }
299
300 println!();
301 println!("Schema:");
302 if let Some(fields) = info.schema.get("fields").and_then(|f| f.as_array()) {
303 for field in fields {
304 let name = field.get("name").and_then(|n| n.as_str()).unwrap_or("?");
305 let dtype = field
306 .get("data_type")
307 .and_then(|d| d.as_str())
308 .unwrap_or("?");
309 let nullable = field
310 .get("nullable")
311 .and_then(serde_json::Value::as_bool)
312 .unwrap_or(true);
313 let null_str = if nullable { "nullable" } else { "not null" };
314 println!(" - {} ({}) [{}]", name, dtype, null_str);
315 }
316 }
317
318 Ok(())
319}
320
321pub(crate) fn cmd_registry_delete(name: &str, version: &str, path: &Path) -> crate::Result<()> {
323 let registry = create_registry(path)?;
324 registry.delete(name, version)?;
325 println!("Deleted {}@{} from registry", name, version);
326 Ok(())
327}
328
329#[cfg(test)]
330#[allow(
331 clippy::cast_possible_truncation,
332 clippy::cast_possible_wrap,
333 clippy::cast_precision_loss,
334 clippy::uninlined_format_args,
335 clippy::unwrap_used,
336 clippy::expect_used,
337 clippy::redundant_clone,
338 clippy::cast_lossless,
339 clippy::redundant_closure_for_method_calls,
340 clippy::too_many_lines,
341 clippy::float_cmp,
342 clippy::similar_names,
343 clippy::needless_late_init,
344 clippy::redundant_pattern_matching
345)]
346mod tests {
347 use std::sync::Arc;
348
349 use arrow::{
350 array::{Int32Array, StringArray},
351 datatypes::{DataType, Field, Schema},
352 };
353
354 use super::*;
355 use crate::ArrowDataset;
356
357 fn create_test_parquet(path: &Path, rows: usize) {
358 let schema = Arc::new(Schema::new(vec![
359 Field::new("id", DataType::Int32, false),
360 Field::new("name", DataType::Utf8, false),
361 ]));
362
363 let ids: Vec<i32> = (0..rows as i32).collect();
364 let names: Vec<String> = ids.iter().map(|i| format!("item_{}", i)).collect();
365
366 let batch = arrow::array::RecordBatch::try_new(
367 schema,
368 vec![
369 Arc::new(Int32Array::from(ids)),
370 Arc::new(StringArray::from(names)),
371 ],
372 )
373 .ok()
374 .unwrap_or_else(|| panic!("Should create batch"));
375
376 let dataset = ArrowDataset::from_batch(batch)
377 .ok()
378 .unwrap_or_else(|| panic!("Should create dataset"));
379
380 dataset
381 .to_parquet(path)
382 .ok()
383 .unwrap_or_else(|| panic!("Should write parquet"));
384 }
385
386 #[test]
387 fn test_cmd_registry_init() {
388 let temp_dir = tempfile::tempdir()
389 .ok()
390 .unwrap_or_else(|| panic!("Should create temp dir"));
391 let registry_path = temp_dir.path().join("registry");
392
393 let result = cmd_registry_init(®istry_path);
394 assert!(result.is_ok());
395 assert!(registry_path.exists());
396 }
397
398 #[test]
399 fn test_cmd_registry_list_empty() {
400 let temp_dir = tempfile::tempdir()
401 .ok()
402 .unwrap_or_else(|| panic!("Should create temp dir"));
403 let registry_path = temp_dir.path().join("registry");
404
405 cmd_registry_init(®istry_path)
407 .ok()
408 .unwrap_or_else(|| panic!("Should init"));
409
410 let result = cmd_registry_list(®istry_path);
411 assert!(result.is_ok());
412 }
413
414 #[test]
415 fn test_cmd_registry_push_and_pull() {
416 let temp_dir = tempfile::tempdir()
417 .ok()
418 .unwrap_or_else(|| panic!("Should create temp dir"));
419 let registry_path = temp_dir.path().join("registry");
420 let input = temp_dir.path().join("data.parquet");
421 let output = temp_dir.path().join("pulled.parquet");
422
423 create_test_parquet(&input, 25);
425
426 let result = cmd_registry_push(
428 &input,
429 "test-dataset",
430 "1.0.0",
431 "A test dataset",
432 "MIT",
433 "test,example",
434 ®istry_path,
435 );
436 assert!(result.is_ok());
437
438 let result = cmd_registry_list(®istry_path);
440 assert!(result.is_ok());
441
442 let result = cmd_registry_pull("test-dataset", &output, Some("1.0.0"), ®istry_path);
444 assert!(result.is_ok());
445 assert!(output.exists());
446
447 let original = ArrowDataset::from_parquet(&input)
449 .ok()
450 .unwrap_or_else(|| panic!("Should load original"));
451 let pulled = ArrowDataset::from_parquet(&output)
452 .ok()
453 .unwrap_or_else(|| panic!("Should load pulled"));
454 assert_eq!(original.len(), pulled.len());
455 }
456
457 #[test]
458 fn test_cmd_registry_search() {
459 let temp_dir = tempfile::tempdir()
460 .ok()
461 .unwrap_or_else(|| panic!("Should create temp dir"));
462 let registry_path = temp_dir.path().join("registry");
463 let input = temp_dir.path().join("data.parquet");
464
465 create_test_parquet(&input, 10);
466
467 cmd_registry_push(
469 &input,
470 "ml-dataset",
471 "1.0.0",
472 "Machine learning training data",
473 "Apache-2.0",
474 "ml,training",
475 ®istry_path,
476 )
477 .ok()
478 .unwrap_or_else(|| panic!("Should push"));
479
480 let result = cmd_registry_search("ml", ®istry_path);
482 assert!(result.is_ok());
483
484 let result = cmd_registry_search("machine", ®istry_path);
486 assert!(result.is_ok());
487 }
488
489 #[test]
490 fn test_cmd_registry_show_info() {
491 let temp_dir = tempfile::tempdir()
492 .ok()
493 .unwrap_or_else(|| panic!("Should create temp dir"));
494 let registry_path = temp_dir.path().join("registry");
495 let input = temp_dir.path().join("data.parquet");
496
497 create_test_parquet(&input, 10);
498
499 cmd_registry_push(
500 &input,
501 "info-test",
502 "1.0.0",
503 "Test description",
504 "MIT",
505 "test",
506 ®istry_path,
507 )
508 .ok()
509 .unwrap_or_else(|| panic!("Should push"));
510
511 let result = cmd_registry_show_info("info-test", ®istry_path);
512 assert!(result.is_ok());
513 }
514
515 #[test]
516 fn test_cmd_registry_delete() {
517 let temp_dir = tempfile::tempdir()
518 .ok()
519 .unwrap_or_else(|| panic!("Should create temp dir"));
520 let registry_path = temp_dir.path().join("registry");
521 let input = temp_dir.path().join("data.parquet");
522
523 create_test_parquet(&input, 10);
524
525 cmd_registry_push(
527 &input,
528 "delete-test",
529 "1.0.0",
530 "Will be deleted",
531 "",
532 "",
533 ®istry_path,
534 )
535 .ok()
536 .unwrap_or_else(|| panic!("Should push"));
537
538 let result = cmd_registry_delete("delete-test", "1.0.0", ®istry_path);
540 assert!(result.is_ok());
541
542 let result = cmd_registry_show_info("delete-test", ®istry_path);
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn test_cmd_registry_pull_latest() {
549 let temp_dir = tempfile::tempdir()
550 .ok()
551 .unwrap_or_else(|| panic!("Should create temp dir"));
552 let registry_path = temp_dir.path().join("registry");
553 let input1 = temp_dir.path().join("v1.parquet");
554 let input2 = temp_dir.path().join("v2.parquet");
555 let output = temp_dir.path().join("pulled.parquet");
556
557 create_test_parquet(&input1, 10);
558 create_test_parquet(&input2, 20);
559
560 cmd_registry_push(&input1, "versioned", "1.0.0", "V1", "", "", ®istry_path)
562 .ok()
563 .unwrap_or_else(|| panic!("Should push v1"));
564
565 cmd_registry_push(&input2, "versioned", "2.0.0", "V2", "", "", ®istry_path)
567 .ok()
568 .unwrap_or_else(|| panic!("Should push v2"));
569
570 let result = cmd_registry_pull("versioned", &output, None, ®istry_path);
572 assert!(result.is_ok());
573
574 let pulled = ArrowDataset::from_parquet(&output)
576 .ok()
577 .unwrap_or_else(|| panic!("Should load"));
578 assert_eq!(pulled.len(), 20);
579 }
580
581 #[test]
582 fn test_cmd_registry_search_no_results() {
583 let temp_dir = tempfile::tempdir()
584 .ok()
585 .unwrap_or_else(|| panic!("Should create temp dir"));
586 let registry_path = temp_dir.path().join("registry");
587
588 cmd_registry_init(®istry_path)
590 .ok()
591 .unwrap_or_else(|| panic!("Should init"));
592
593 let result = cmd_registry_search("nonexistent-dataset-xyz", ®istry_path);
595 assert!(result.is_ok());
596 }
597
598 #[test]
599 fn test_cmd_registry_push_with_long_description() {
600 let temp_dir = tempfile::tempdir()
601 .ok()
602 .unwrap_or_else(|| panic!("Should create temp dir"));
603 let registry_path = temp_dir.path().join("registry");
604 let input = temp_dir.path().join("data.parquet");
605
606 create_test_parquet(&input, 10);
607
608 let long_desc = "This is a very long description that exceeds thirty characters and will be truncated in the list view";
610 let result = cmd_registry_push(
611 &input,
612 "long-desc-test",
613 "1.0.0",
614 long_desc,
615 "MIT",
616 "",
617 ®istry_path,
618 );
619 assert!(result.is_ok());
620
621 let result = cmd_registry_list(®istry_path);
623 assert!(result.is_ok());
624 }
625
626 #[test]
627 fn test_cmd_registry_show_info_with_all_metadata() {
628 let temp_dir = tempfile::tempdir()
629 .ok()
630 .unwrap_or_else(|| panic!("Should create temp dir"));
631 let registry_path = temp_dir.path().join("registry");
632 let input = temp_dir.path().join("data.parquet");
633
634 create_test_parquet(&input, 10);
635
636 cmd_registry_push(
638 &input,
639 "full-metadata",
640 "1.0.0",
641 "Full metadata test",
642 "Apache-2.0",
643 "test,metadata,full",
644 ®istry_path,
645 )
646 .ok()
647 .unwrap_or_else(|| panic!("Should push"));
648
649 let result = cmd_registry_show_info("full-metadata", ®istry_path);
650 assert!(result.is_ok());
651 }
652
653 #[test]
654 fn test_create_registry_new_directory() {
655 let temp_dir = tempfile::tempdir()
656 .ok()
657 .unwrap_or_else(|| panic!("Should create temp dir"));
658 let registry_path = temp_dir.path().join("new_registry_dir");
659
660 assert!(!registry_path.exists());
662
663 let result = create_registry(®istry_path);
664 assert!(result.is_ok());
665
666 assert!(registry_path.exists());
668 }
669
670 #[test]
671 fn test_cmd_registry_delete_nonexistent() {
672 let temp_dir = tempfile::tempdir()
673 .ok()
674 .unwrap_or_else(|| panic!("Should create temp dir"));
675 let registry_path = temp_dir.path().join("registry");
676
677 cmd_registry_init(®istry_path)
679 .ok()
680 .unwrap_or_else(|| panic!("Should init"));
681
682 let result = cmd_registry_delete("nonexistent", "1.0.0", ®istry_path);
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn test_cmd_registry_pull_nonexistent() {
689 let temp_dir = tempfile::tempdir()
690 .ok()
691 .unwrap_or_else(|| panic!("Should create temp dir"));
692 let registry_path = temp_dir.path().join("registry");
693 let output = temp_dir.path().join("output.parquet");
694
695 cmd_registry_init(®istry_path)
697 .ok()
698 .unwrap_or_else(|| panic!("Should init"));
699
700 let result = cmd_registry_pull("nonexistent", &output, None, ®istry_path);
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_cmd_registry_search_with_data() {
707 let temp_dir = tempfile::tempdir()
708 .ok()
709 .unwrap_or_else(|| panic!("Should create temp dir"));
710 let registry_path = temp_dir.path().join("registry");
711 let data_path = temp_dir.path().join("data.parquet");
712
713 create_test_parquet(&data_path, 20);
714
715 cmd_registry_push(
717 &data_path,
718 "searchable-data",
719 "1.0.0",
720 "Dataset for search test",
721 "MIT",
722 "search,test",
723 ®istry_path,
724 )
725 .unwrap();
726
727 let result = cmd_registry_search("search", ®istry_path);
728 assert!(result.is_ok());
729 }
730
731 #[test]
732 fn test_cmd_registry_search_empty_results() {
733 let temp_dir = tempfile::tempdir()
734 .ok()
735 .unwrap_or_else(|| panic!("Should create temp dir"));
736 let registry_path = temp_dir.path().join("registry");
737
738 cmd_registry_init(®istry_path).unwrap();
739
740 let result = cmd_registry_search("nonexistent", ®istry_path);
741 assert!(result.is_ok());
742 }
743
744 #[test]
745 fn test_cmd_registry_show_info_basic() {
746 let temp_dir = tempfile::tempdir()
747 .ok()
748 .unwrap_or_else(|| panic!("Should create temp dir"));
749 let registry_path = temp_dir.path().join("registry");
750 let data_path = temp_dir.path().join("data.parquet");
751
752 create_test_parquet(&data_path, 20);
753
754 cmd_registry_push(
755 &data_path,
756 "info-dataset",
757 "1.0.0",
758 "Dataset for info test",
759 "Apache-2.0",
760 "info,test",
761 ®istry_path,
762 )
763 .unwrap();
764
765 let result = cmd_registry_show_info("info-dataset", ®istry_path);
766 assert!(result.is_ok());
767 }
768
769 #[test]
770 fn test_cmd_registry_show_info_not_found() {
771 let temp_dir = tempfile::tempdir()
772 .ok()
773 .unwrap_or_else(|| panic!("Should create temp dir"));
774 let registry_path = temp_dir.path().join("registry");
775
776 cmd_registry_init(®istry_path).unwrap();
777
778 let result = cmd_registry_show_info("nonexistent", ®istry_path);
779 assert!(result.is_err());
780 }
781
782 #[test]
783 fn test_cmd_registry_delete_existing() {
784 let temp_dir = tempfile::tempdir()
785 .ok()
786 .unwrap_or_else(|| panic!("Should create temp dir"));
787 let registry_path = temp_dir.path().join("registry");
788 let data_path = temp_dir.path().join("data.parquet");
789
790 create_test_parquet(&data_path, 20);
791
792 cmd_registry_push(
793 &data_path,
794 "delete-test",
795 "1.0.0",
796 "Dataset to delete",
797 "MIT",
798 "delete,test",
799 ®istry_path,
800 )
801 .unwrap();
802
803 let result = cmd_registry_delete("delete-test", "1.0.0", ®istry_path);
804 assert!(result.is_ok());
805 }
806}