actr_cli/commands/
discovery.rs

1//! Discovery Command Implementation
2//!
3//! Demonstrates multi-level reuse patterns: Service Discovery -> Validation -> Optional Install
4
5use anyhow::Result;
6use async_trait::async_trait;
7use clap::Args;
8
9use crate::core::{
10    ActrCliError, Command, CommandContext, CommandResult, ComponentType, ConfigManager,
11    DependencyResolver, DependencySpec, Fingerprint, FingerprintValidator, NetworkValidator,
12    ResolvedDependency, ServiceDetails, ServiceDiscovery, ServiceInfo,
13};
14
15/// Discovery ๅ‘ฝไปค
16#[derive(Args, Debug)]
17#[command(
18    about = "Discover network services",
19    long_about = "Discover Actor services in the network, view available services and choose to install"
20)]
21pub struct DiscoveryCommand {
22    /// Service name filter pattern (e.g., user-*)
23    #[arg(long, value_name = "PATTERN")]
24    pub filter: Option<String>,
25
26    /// Show detailed information
27    #[arg(long)]
28    pub verbose: bool,
29
30    /// Automatically install selected services
31    #[arg(long)]
32    pub auto_install: bool,
33}
34
35#[async_trait]
36impl Command for DiscoveryCommand {
37    async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
38        // Get reusable components
39        let (service_discovery, user_interface, config_manager) = {
40            let container = context.container.lock().unwrap();
41            (
42                container.get_service_discovery()?,
43                container.get_user_interface()?,
44                container.get_config_manager()?,
45            )
46        };
47
48        // Phase 1: Service Discovery
49
50        let filter = self.create_service_filter();
51        let services = service_discovery.discover_services(filter.as_ref()).await?;
52        tracing::debug!("Discovered services: {:?}", services);
53
54        if services.is_empty() {
55            println!("โ„น๏ธ No available Actor services discovered in the current network");
56            return Ok(CommandResult::Success("No services discovered".to_string()));
57        }
58
59        println!("๐Ÿ” Discovered Actor services:");
60        // Display discovered services table
61        self.display_services_table(&services);
62
63        // Selection Phase
64        let service_options: Vec<String> = services.iter().map(|s| s.name.clone()).collect();
65
66        let selected_index = match user_interface
67            .select_from_list(&service_options, "Select a service to view (Esc to quit)")
68            .await
69        {
70            Ok(index) => index,
71            Err(err) if Self::is_operation_cancelled(&err) => {
72                return Ok(CommandResult::Success("Operation cancelled".to_string()));
73            }
74            Err(err) => return Err(err),
75        };
76
77        let selected_service = &services[selected_index];
78        let mut selected_details = None;
79
80        if self.verbose {
81            let details = service_discovery
82                .get_service_details(&selected_service.name)
83                .await?;
84            self.display_service_details(&details);
85            selected_details = Some(details);
86        }
87
88        // Action menu prompt
89        let menu_prompt = format!("Options for {}", selected_service.name);
90
91        // Action menu items (as shown in screenshot)
92        let action_menu = vec![
93            "[1] View service details (fingerprint, publication time)".to_string(),
94            "[2] Export proto files".to_string(),
95            "[3] Add to configuration file".to_string(),
96        ];
97
98        let action_choice = match user_interface
99            .select_from_list(&action_menu, &menu_prompt)
100            .await
101        {
102            Ok(choice) => choice,
103            Err(err) if Self::is_operation_cancelled(&err) => {
104                return Ok(CommandResult::Success("Operation cancelled".to_string()));
105            }
106            Err(err) => return Err(err),
107        };
108
109        match action_choice {
110            0 => {
111                if let Some(details) = selected_details.as_ref() {
112                    self.display_service_details(details);
113                } else {
114                    let details = service_discovery
115                        .get_service_details(&selected_service.name)
116                        .await?;
117                    self.display_service_details(&details);
118                }
119                Ok(CommandResult::Success(
120                    "Service details displayed".to_string(),
121                ))
122            }
123            1 => {
124                // Export proto files
125                self.export_proto_files(selected_service, &service_discovery, &config_manager)
126                    .await?;
127                Ok(CommandResult::Success("Proto files exported".to_string()))
128            }
129            2 => {
130                // Add to configuration file - core flow of reuse architecture
131                self.add_to_config_with_validation(selected_service, context)
132                    .await
133            }
134            _ => Ok(CommandResult::Success("Invalid choice".to_string())),
135        }
136    }
137
138    fn required_components(&self) -> Vec<ComponentType> {
139        // Components needed for Discovery command
140        vec![
141            ComponentType::ServiceDiscovery, // Core service discovery
142            ComponentType::UserInterface,    // User interface
143            ComponentType::ConfigManager,    // Configuration management
144            ComponentType::DependencyResolver,
145            ComponentType::NetworkValidator,
146            ComponentType::FingerprintValidator,
147        ]
148    }
149
150    fn name(&self) -> &str {
151        "discovery"
152    }
153
154    fn description(&self) -> &str {
155        "Discover available Actor services in the network (Reuse architecture + check-first)"
156    }
157}
158
159impl DiscoveryCommand {
160    pub fn new(filter: Option<String>, verbose: bool, auto_install: bool) -> Self {
161        Self {
162            filter,
163            verbose,
164            auto_install,
165        }
166    }
167
168    // Create from clap Args
169    pub fn from_args(args: &DiscoveryCommand) -> Self {
170        DiscoveryCommand {
171            filter: args.filter.clone(),
172            verbose: args.verbose,
173            auto_install: args.auto_install,
174        }
175    }
176
177    /// Create service filter
178    fn create_service_filter(&self) -> Option<crate::core::ServiceFilter> {
179        self.filter
180            .as_ref()
181            .map(|pattern| crate::core::ServiceFilter {
182                name_pattern: Some(pattern.clone()),
183                version_range: None,
184                tags: None,
185            })
186    }
187
188    fn is_operation_cancelled(err: &anyhow::Error) -> bool {
189        matches!(
190            err.downcast_ref::<ActrCliError>(),
191            Some(ActrCliError::OperationCancelled)
192        )
193    }
194
195    #[allow(clippy::too_many_arguments)]
196    async fn validate_dependency(
197        &self,
198        service: &ServiceInfo,
199        dependency_spec: &DependencySpec,
200        expected_fingerprint: Option<&str>,
201        check_conflicts: bool,
202        existing_specs: &[DependencySpec],
203        dependency_resolver: &std::sync::Arc<dyn DependencyResolver>,
204        service_discovery: &std::sync::Arc<dyn ServiceDiscovery>,
205        network_validator: &std::sync::Arc<dyn NetworkValidator>,
206        fingerprint_validator: &std::sync::Arc<dyn FingerprintValidator>,
207    ) -> Result<()> {
208        println!();
209        println!("๐Ÿ” Validating dependency...");
210
211        let mut failures = Vec::new();
212
213        match service_discovery
214            .check_service_availability(&service.name)
215            .await
216        {
217            Ok(status) => {
218                if status.is_available {
219                    println!("  โ”œโ”€ โœ… Service availability");
220                } else {
221                    println!("  โ”œโ”€ โŒ Service availability");
222                    failures.push(format!("Service '{}' not found in registry", service.name));
223                }
224            }
225            Err(e) => {
226                println!("  โ”œโ”€ โŒ Service availability");
227                failures.push(format!("Service availability check failed: {e}"));
228            }
229        }
230
231        match network_validator.check_connectivity(&service.name).await {
232            Ok(connectivity) => {
233                if connectivity.is_reachable {
234                    println!("  โ”œโ”€ โœ… Network connectivity");
235                } else {
236                    println!("  โ”œโ”€ โŒ Network connectivity");
237                    let detail = connectivity.error.as_deref().unwrap_or("unknown error");
238                    failures.push(format!(
239                        "Network connectivity failed for '{}': {}",
240                        service.name, detail
241                    ));
242                }
243            }
244            Err(e) => {
245                println!("  โ”œโ”€ โŒ Network connectivity");
246                failures.push(format!("Network connectivity check failed: {e}"));
247            }
248        }
249
250        if let Some(expected_fingerprint) = expected_fingerprint.filter(|fp| !fp.is_empty()) {
251            match fingerprint_validator
252                .compute_service_fingerprint(service)
253                .await
254            {
255                Ok(actual) => {
256                    let expected = Fingerprint {
257                        algorithm: actual.algorithm.clone(),
258                        value: expected_fingerprint.to_string(),
259                    };
260                    let is_valid = fingerprint_validator
261                        .verify_fingerprint(&expected, &actual)
262                        .await
263                        .unwrap_or(false);
264                    if is_valid {
265                        println!("  โ”œโ”€ โœ… Fingerprint match");
266                    } else {
267                        println!("  โ”œโ”€ โŒ Fingerprint match");
268                        failures.push(format!("Fingerprint mismatch for '{}'", service.name));
269                    }
270                }
271                Err(e) => {
272                    println!("  โ”œโ”€ โŒ Fingerprint check");
273                    failures.push(format!("Fingerprint check failed: {e}"));
274                }
275            }
276        } else {
277            println!("  โ”œโ”€ โš ๏ธ  Fingerprint missing; skipping check");
278        }
279
280        if check_conflicts {
281            let mut resolved = Vec::with_capacity(existing_specs.len() + 1);
282            for spec in existing_specs {
283                resolved.push(ResolvedDependency {
284                    spec: spec.clone(),
285                    fingerprint: spec.fingerprint.clone().unwrap_or_default(),
286                    proto_files: Vec::new(),
287                });
288            }
289            resolved.push(ResolvedDependency {
290                spec: dependency_spec.clone(),
291                fingerprint: dependency_spec.fingerprint.clone().unwrap_or_default(),
292                proto_files: Vec::new(),
293            });
294
295            match dependency_resolver.check_conflicts(&resolved).await {
296                Ok(conflicts) => {
297                    if conflicts.is_empty() {
298                        println!("  โ”œโ”€ โœ… Dependency conflicts");
299                    } else {
300                        println!("  โ”œโ”€ โŒ Dependency conflicts");
301                        let details = conflicts
302                            .iter()
303                            .map(|conflict| conflict.description.clone())
304                            .collect::<Vec<_>>()
305                            .join(", ");
306                        failures.push(format!("Dependency conflicts: {details}"));
307                    }
308                }
309                Err(e) => {
310                    println!("  โ”œโ”€ โŒ Dependency conflicts");
311                    failures.push(format!("Dependency conflict check failed: {e}"));
312                }
313            }
314        } else {
315            println!("  โ”œโ”€ โš ๏ธ  Dependency conflict check skipped (already configured)");
316        }
317
318        if failures.is_empty() {
319            println!("  โ””โ”€ โœ… Validation passed");
320            Ok(())
321        } else {
322            println!("  โ””โ”€ โŒ Validation failed");
323            Err(ActrCliError::ValidationFailed {
324                details: failures.join("; "),
325            }
326            .into())
327        }
328    }
329
330    /// Display services table
331    fn display_services_table(&self, services: &[ServiceInfo]) {
332        println!();
333        // Total width limit is 160
334        const TOTAL_MAX_WIDTH: usize = 160;
335        // Border and separator overhead
336        const BORDER_OVERHEAD: usize = 7;
337
338        // Calculate the maximum width of each column
339        let name_width = services
340            .iter()
341            .map(|s| s.name.chars().count())
342            .max()
343            .unwrap_or(0)
344            .max("Service Name".len());
345
346        let tags_width = services
347            .iter()
348            .map(|s| s.tags.join(", ").chars().count())
349            .max()
350            .unwrap_or(0)
351            .max("Tags".len());
352
353        let desc_width = services
354            .iter()
355            .map(|s| {
356                s.description
357                    .as_deref()
358                    .unwrap_or("No description")
359                    .chars()
360                    .count()
361            })
362            .max()
363            .unwrap_or(0)
364            .max("Description".len());
365
366        let name_w = name_width;
367        let tags_w = tags_width;
368        let mut desc_w = desc_width;
369
370        // If the total width is exceeded, truncate the Description
371        if name_w + tags_w + desc_w + BORDER_OVERHEAD > TOTAL_MAX_WIDTH {
372            let available = TOTAL_MAX_WIDTH - BORDER_OVERHEAD;
373            let used = name_w + tags_w;
374            desc_w = available.saturating_sub(used).max(10); // Description ่‡ณๅฐ‘ 10 ๅญ—็ฌฆ
375        }
376
377        // Generate table header
378        let top_border = format!(
379            "โ”Œโ”€{}โ”€โ”ฌโ”€{}โ”€โ”ฌโ”€{}โ”€โ”",
380            "โ”€".repeat(name_w),
381            "โ”€".repeat(tags_w),
382            "โ”€".repeat(desc_w)
383        );
384        let header = format!(
385            "โ”‚ {:width$} โ”‚ {:tags_w$} โ”‚ {:desc_w$} โ”‚",
386            "Service Name",
387            "Tags",
388            "Description",
389            width = name_w,
390            tags_w = tags_w,
391            desc_w = desc_w
392        );
393        let separator = format!(
394            "โ”œโ”€{}โ”€โ”ผโ”€{}โ”€โ”ผโ”€{}โ”€โ”ค",
395            "โ”€".repeat(name_w),
396            "โ”€".repeat(tags_w),
397            "โ”€".repeat(desc_w)
398        );
399        let bottom_border = format!(
400            "โ””โ”€{}โ”€โ”ดโ”€{}โ”€โ”ดโ”€{}โ”€โ”˜",
401            "โ”€".repeat(name_w),
402            "โ”€".repeat(tags_w),
403            "โ”€".repeat(desc_w)
404        );
405
406        println!("{top_border}");
407        println!("{header}");
408        println!("{separator}");
409
410        for service in services {
411            let tags_str = service.tags.join(", ");
412            let description = service
413                .description
414                .as_deref()
415                .unwrap_or("No description")
416                .chars()
417                .take(desc_w)
418                .collect::<String>();
419
420            println!(
421                "โ”‚ {:name_w$} โ”‚ {:tags_w$} โ”‚ {:desc_w$} โ”‚",
422                service.name,
423                tags_str.chars().take(tags_w).collect::<String>(),
424                description,
425                name_w = name_w,
426                tags_w = tags_w,
427                desc_w = desc_w
428            );
429        }
430
431        println!("{bottom_border}");
432        println!();
433    }
434
435    /// Display service info
436    fn display_service_info(&self, service: &ServiceInfo) {
437        println!("๐Ÿ“‹ Selected service: {}", service.name);
438        if let Some(desc) = &service.description {
439            println!("๐Ÿ“ Description: {desc}");
440        }
441        println!("๐Ÿ” Fingerprint: {}", service.fingerprint);
442        let time = service
443            .published_at
444            .and_then(|published_at| chrono::DateTime::from_timestamp(published_at, 0))
445            .map(|dt| {
446                dt.with_timezone(&chrono::Local)
447                    .format("%Y-%m-%d %H:%M:%S")
448                    .to_string()
449            })
450            .unwrap_or_else(|| "Unknown".to_string());
451        println!("๐Ÿ“… Publication Time: {}", time);
452        println!(
453            "๐Ÿท๏ธ  Tags: {}",
454            if service.tags.is_empty() {
455                "(none)".to_string()
456            } else {
457                service.tags.join(", ")
458            }
459        );
460        println!("๐Ÿ“Š Methods count: {}", service.methods.len());
461        println!();
462    }
463
464    #[allow(unused)]
465    /// Display service details
466    fn display_service_details(&self, details: &ServiceDetails) {
467        println!("๐Ÿ“– {} Detailed Information:", details.info.name);
468        println!("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
469        self.display_service_info(&details.info);
470        println!("๐Ÿ“‹ Available Methods:");
471        if details.info.methods.is_empty() {
472            println!("  (None)");
473        } else {
474            for method in &details.info.methods {
475                println!(
476                    "  โ€ข {}: {} โ†’ {}",
477                    method.name, method.input_type, method.output_type
478                );
479            }
480        }
481
482        if !details.dependencies.is_empty() {
483            println!();
484            println!("๐Ÿ”— Dependent Services:");
485            for dep in &details.dependencies {
486                println!("  โ€ข {dep}");
487            }
488        }
489
490        println!();
491        println!("๐Ÿ“ Proto Files:");
492        if details.proto_files.is_empty() {
493            println!("  (None)");
494        } else {
495            for proto in &details.proto_files {
496                println!("  โ€ข {} ({} services)", proto.name, proto.services.len());
497            }
498        }
499
500        println!();
501    }
502
503    /// Export proto files
504    async fn export_proto_files(
505        &self,
506        service: &ServiceInfo,
507        service_discovery: &std::sync::Arc<dyn ServiceDiscovery>,
508        config_manager: &std::sync::Arc<dyn ConfigManager>,
509    ) -> Result<()> {
510        println!("๐Ÿ“ค Exporting proto files for {}...", service.name);
511
512        let proto_files = service_discovery.get_service_proto(&service.name).await?;
513
514        let output_dir = config_manager
515            .get_project_root()
516            .join("exports")
517            .join("remote")
518            .join(&service.name);
519        std::fs::create_dir_all(&output_dir)?;
520
521        for proto in &proto_files {
522            let file_path = output_dir.join(&proto.name);
523            if let Some(parent) = file_path.parent() {
524                std::fs::create_dir_all(parent)?;
525            }
526            std::fs::write(&file_path, &proto.content)?;
527            println!("โœ… Exported: {}", file_path.display());
528        }
529
530        println!("๐ŸŽ‰ Export completed, total {} files", proto_files.len());
531        Ok(())
532    }
533
534    /// Add to configuration file - core flow of reuse architecture
535    async fn add_to_config_with_validation(
536        &self,
537        service: &ServiceInfo,
538        context: &CommandContext,
539    ) -> Result<CommandResult> {
540        let (
541            config_manager,
542            user_interface,
543            dependency_resolver,
544            service_discovery,
545            network_validator,
546            fingerprint_validator,
547        ) = {
548            let container = context.container.lock().unwrap();
549            (
550                container.get_config_manager()?,
551                container.get_user_interface()?,
552                container.get_dependency_resolver()?,
553                container.get_service_discovery()?,
554                container.get_network_validator()?,
555                container.get_fingerprint_validator()?,
556            )
557        };
558
559        // Convert to dependency spec
560        let dependency_spec = DependencySpec {
561            alias: service.name.clone(),
562            actr_type: Some(service.actr_type.clone()),
563            name: service.name.clone(),
564            fingerprint: Some(service.fingerprint.clone()),
565        };
566
567        // Check if a dependency with the same name already exists
568        let config = config_manager
569            .load_config(
570                config_manager
571                    .get_project_root()
572                    .join("Actr.toml")
573                    .as_path(),
574            )
575            .await?;
576
577        let existing_by_name = config
578            .dependencies
579            .iter()
580            .find(|dep| dep.name == service.name);
581        let existing_by_alias = config
582            .dependencies
583            .iter()
584            .find(|dep| dep.alias == dependency_spec.alias);
585
586        if let Some(existing) = existing_by_alias
587            && existing.name != service.name
588        {
589            return Err(ActrCliError::Dependency {
590                message: format!(
591                    "Dependency alias '{}' already exists for '{}'",
592                    existing.alias, existing.name
593                ),
594            }
595            .into());
596        }
597
598        let should_update_config = existing_by_name.is_none();
599        if let Some(existing) = existing_by_name {
600            println!(
601                "โ„น๏ธ  Dependency with name '{}' already exists (alias: '{}')",
602                service.name, existing.alias
603            );
604            if let (Some(existing_fp), Some(discovered_fp)) = (
605                existing.fingerprint.as_deref(),
606                dependency_spec.fingerprint.as_deref(),
607            ) && existing_fp != discovered_fp
608            {
609                println!(
610                    "โš ๏ธ  Fingerprint mismatch: config '{}' vs discovery '{}'",
611                    existing_fp, discovered_fp
612                );
613            }
614            println!("   Skipping configuration update");
615        }
616
617        let expected_fingerprint = existing_by_name
618            .and_then(|dep| dep.fingerprint.clone())
619            .or_else(|| dependency_spec.fingerprint.clone());
620        let existing_specs = dependency_resolver.resolve_spec(&config).await?;
621        self.validate_dependency(
622            service,
623            &dependency_spec,
624            expected_fingerprint.as_deref(),
625            should_update_config,
626            &existing_specs,
627            &dependency_resolver,
628            &service_discovery,
629            &network_validator,
630            &fingerprint_validator,
631        )
632        .await?;
633
634        if should_update_config {
635            println!("๐Ÿ“ Adding {} to configuration file...", service.name);
636            let backup = config_manager.backup_config().await?;
637            match config_manager.update_dependency(&dependency_spec).await {
638                Ok(_) => {
639                    config_manager.remove_backup(backup).await?;
640                    println!("โœ… Added {} to configuration file", service.name);
641                }
642                Err(e) => {
643                    config_manager.restore_backup(backup).await?;
644                    return Err(ActrCliError::Config {
645                        message: format!("Configuration update failed: {e}"),
646                    }
647                    .into());
648                }
649            }
650        }
651
652        // Ask if user wants to install immediately
653        println!();
654        let should_install = if self.auto_install {
655            true
656        } else {
657            user_interface
658                .confirm("๐Ÿค” Install this dependency now?")
659                .await?
660        };
661
662        if should_install {
663            // Reuse install flow
664            println!();
665            println!("๐Ÿ“ฆ Installing {}...", service.name);
666
667            let install_pipeline = {
668                let mut container = context.container.lock().unwrap();
669                match container.get_install_pipeline() {
670                    Ok(pipeline) => pipeline,
671                    Err(_) => {
672                        println!("โ„น๏ธ Install pipeline is not implemented yet; skipping.");
673                        return Ok(CommandResult::Success(
674                            "Dependency added; install pending".to_string(),
675                        ));
676                    }
677                }
678            };
679
680            match install_pipeline
681                .install_dependencies(&[dependency_spec])
682                .await
683            {
684                Ok(install_result) => {
685                    println!("  โ”œโ”€ ๐Ÿ“ฆ Cache proto files โœ…");
686                    println!("  โ”œโ”€ ๐Ÿ”’ Update lock file โœ…");
687                    println!("  โ””โ”€ โœ… Installation complete");
688                    println!();
689                    println!("๐Ÿ’ก Tip: Run 'actr gen' to generate the latest code");
690
691                    Ok(CommandResult::Install(install_result))
692                }
693                Err(e) => {
694                    eprintln!("โŒ Installation failed: {e}");
695                    Ok(CommandResult::Success(
696                        "Dependency added but installation failed".to_string(),
697                    ))
698                }
699            }
700        } else {
701            println!("โœ… Dependency added to configuration file");
702            println!("๐Ÿ’ก Tip: Run 'actr install' to install dependencies");
703            Ok(CommandResult::Success(
704                "Dependency added to configuration".to_string(),
705            ))
706        }
707    }
708}
709
710impl Default for DiscoveryCommand {
711    fn default() -> Self {
712        Self::new(None, false, false)
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn test_create_service_filter() {
722        let cmd = DiscoveryCommand::new(Some("user-*".to_string()), false, false);
723        let filter = cmd.create_service_filter();
724
725        assert!(filter.is_some());
726        let filter = filter.unwrap();
727        assert_eq!(filter.name_pattern, Some("user-*".to_string()));
728    }
729
730    #[test]
731    fn test_create_service_filter_none() {
732        let cmd = DiscoveryCommand::new(None, false, false);
733        let filter = cmd.create_service_filter();
734
735        assert!(filter.is_none());
736    }
737
738    #[test]
739    fn test_required_components() {
740        let cmd = DiscoveryCommand::default();
741        let components = cmd.required_components();
742
743        // Discovery command requires validation components for check-first flow.
744        assert!(components.contains(&ComponentType::ServiceDiscovery));
745        assert!(components.contains(&ComponentType::UserInterface));
746        assert!(components.contains(&ComponentType::ConfigManager));
747        assert!(components.contains(&ComponentType::DependencyResolver));
748        assert!(components.contains(&ComponentType::NetworkValidator));
749        assert!(components.contains(&ComponentType::FingerprintValidator));
750        assert!(!components.contains(&ComponentType::CacheManager));
751        assert!(!components.contains(&ComponentType::ProtoProcessor));
752    }
753}