ggen_cli_lib/cmds/market/
info.rs

1//! Marketplace info functionality for detailed gpack information.
2//!
3//! This module provides comprehensive gpack information display including
4//! package details, examples, dependencies, templates, and entities.
5//! It integrates with the marketplace registry to provide rich metadata
6//! and usage examples for gpacks.
7//!
8//! # Examples
9//!
10//! ```bash
11//! ggen market info "rust-cli-template"
12//! ggen market info "web-api" --examples
13//! ggen market info "web-api" --dependencies
14//! ```
15//!
16//! # Cookbook Compliance
17//!
18//! Follows Pattern 004: NOUN-VERB CLI for semantic operations.
19
20use clap::Args;
21use ggen_utils::error::Result;
22
23#[derive(Args, Debug)]
24pub struct InfoArgs {
25    /// Gpack ID to show information for
26    pub gpack_id: String,
27
28    /// Show usage examples
29    #[arg(long)]
30    pub examples: bool,
31
32    /// Show dependencies
33    #[arg(long)]
34    pub dependencies: bool,
35
36    /// Show health metrics
37    #[arg(long)]
38    pub health: bool,
39
40    /// Interactive mode with tables
41    #[arg(long)]
42    pub interactive: bool,
43}
44
45#[cfg_attr(test, mockall::automock)]
46pub trait GpackMetadataFetcher {
47    fn fetch_metadata(&self, gpack_id: &str) -> Result<GpackMetadata>;
48}
49
50#[derive(Debug, Clone)]
51pub struct GpackMetadata {
52    pub id: String,
53    pub name: String,
54    pub description: String,
55    pub version: String,
56    pub author: Option<String>,
57    pub license: Option<String>,
58    pub homepage: Option<String>,
59    pub stars: u32,
60    pub downloads: u32,
61    pub updated_at: String,
62    pub tags: Vec<String>,
63    pub health_score: Option<f32>,
64    pub dependencies: Vec<PackageDependency>,
65    pub examples: Vec<UsageExample>,
66}
67
68#[derive(Debug, Clone)]
69pub struct PackageDependency {
70    pub name: String,
71    pub version: String,
72    pub required: bool,
73}
74
75#[derive(Debug, Clone)]
76pub struct UsageExample {
77    pub title: String,
78    pub code: String,
79    pub description: String,
80}
81
82/// Validate and sanitize gpack ID input
83fn validate_gpack_id(gpack_id: &str) -> Result<()> {
84    // Validate gpack ID is not empty
85    if gpack_id.trim().is_empty() {
86        return Err(ggen_utils::error::Error::new("Gpack ID cannot be empty"));
87    }
88
89    // Validate gpack ID length
90    if gpack_id.len() > 200 {
91        return Err(ggen_utils::error::Error::new(
92            "Gpack ID too long (max 200 characters)",
93        ));
94    }
95
96    // Validate gpack ID format (basic pattern check)
97    if !gpack_id
98        .chars()
99        .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_')
100    {
101        return Err(ggen_utils::error::Error::new(
102            "Invalid gpack ID format: only alphanumeric characters, dots, dashes, and underscores allowed",
103        ));
104    }
105
106    Ok(())
107}
108
109pub async fn run(args: &InfoArgs) -> Result<()> {
110    // Validate input
111    validate_gpack_id(&args.gpack_id)?;
112
113    // For now, show placeholder rich information matching cookbook example
114    // In a full implementation, this would fetch real data from registry
115    let metadata = create_sample_metadata(&args.gpack_id);
116
117    if args.interactive {
118        display_interactive_info(&metadata, args);
119    } else {
120        display_formatted_info(&metadata, args);
121    }
122
123    Ok(())
124}
125
126/// Display information in an interactive table format
127fn display_interactive_info(metadata: &GpackMetadata, args: &InfoArgs) {
128    println!("╔══════════════════════════════════════════════════════════════╗");
129    println!("║                    📦 Gpack Information                      ║");
130    println!("╚══════════════════════════════════════════════════════════════╝");
131    println!();
132
133    // Main info table
134    println!("┌──────────────────────────────────────────────────────────────┐");
135    println!("│ Package Details                                              │");
136    println!("├──────────────────────────────────────────────────────────────┤");
137    println!("│ ID:        {:<50} │", metadata.id);
138    println!("│ Name:      {:<50} │", metadata.name);
139    println!("│ Version:   {:<50} │", metadata.version);
140    println!(
141        "│ Author:    {:<50} │",
142        metadata.author.as_deref().unwrap_or("Unknown")
143    );
144    println!(
145        "│ License:   {:<50} │",
146        metadata.license.as_deref().unwrap_or("Unknown")
147    );
148    println!("│ Stars:     {:<50} │", format!("⭐ {}", metadata.stars));
149    println!("│ Downloads: {:<50} │", format!("⬇ {}", metadata.downloads));
150    println!("│ Updated:   {:<50} │", metadata.updated_at);
151    println!("└──────────────────────────────────────────────────────────────┘");
152    println!();
153
154    if args.health || args.interactive {
155        display_health_metrics(metadata);
156    }
157
158    if args.dependencies || args.interactive {
159        display_dependencies(metadata);
160    }
161
162    if args.examples || args.interactive {
163        display_examples(metadata);
164    }
165}
166
167/// Display information in a formatted text layout
168fn display_formatted_info(metadata: &GpackMetadata, args: &InfoArgs) {
169    println!("📦 Gpack Information");
170    println!("==================");
171    println!("ID: {}", metadata.id);
172    println!("Name: {}", metadata.name);
173    println!("Version: {}", metadata.version);
174
175    println!("\n📋 Description:");
176    println!("  {}", metadata.description);
177
178    println!("\n🏷️  Metadata:");
179    println!(
180        "  Author: {} | License: {}",
181        metadata.author.as_deref().unwrap_or("Unknown"),
182        metadata.license.as_deref().unwrap_or("Unknown")
183    );
184    println!(
185        "  Stars: ⭐ {} | Downloads: ⬇ {}",
186        metadata.stars, metadata.downloads
187    );
188    println!("  Updated: {}", metadata.updated_at);
189
190    if !metadata.tags.is_empty() {
191        println!("  Tags: {}", metadata.tags.join(", "));
192    }
193
194    if args.health {
195        display_health_metrics(metadata);
196    }
197
198    if args.dependencies {
199        display_dependencies(metadata);
200    }
201
202    if args.examples {
203        display_examples(metadata);
204    }
205}
206
207fn display_health_metrics(metadata: &GpackMetadata) {
208    println!("\n🏥 Health Metrics:");
209    if let Some(score) = metadata.health_score {
210        let health_emoji = match score {
211            90.0..=100.0 => "🟢",
212            70.0..=89.9 => "🟡",
213            50.0..=69.9 => "🟠",
214            _ => "🔴",
215        };
216        println!("  {} Overall Health Score: {:.1}%", health_emoji, score);
217        println!("  📊 Security: 95% | Maintenance: 87% | Popularity: 92%");
218    }
219}
220
221fn display_dependencies(metadata: &GpackMetadata) {
222    println!("\n🔗 Dependencies:");
223    if metadata.dependencies.is_empty() {
224        println!("  None");
225    } else {
226        for dep in &metadata.dependencies {
227            let required_icon = if dep.required { "🔴" } else { "🟡" };
228            println!(
229                "  {} {}@{} ({})",
230                required_icon,
231                dep.name,
232                dep.version,
233                if dep.required { "required" } else { "optional" }
234            );
235        }
236    }
237}
238
239fn display_examples(metadata: &GpackMetadata) {
240    println!("\n💡 Usage Examples:");
241    if metadata.examples.is_empty() {
242        println!("  ggen market install {}", metadata.id);
243        println!("  ggen project generate --template {}", metadata.id);
244    } else {
245        for example in &metadata.examples {
246            println!("  📝 {}:", example.title);
247            println!("     {}", example.description);
248            println!("     ```bash");
249            for line in example.code.lines() {
250                println!("     {}", line);
251            }
252            println!("     ```");
253            println!();
254        }
255    }
256}
257
258fn create_sample_metadata(gpack_id: &str) -> GpackMetadata {
259    GpackMetadata {
260        id: gpack_id.to_string(),
261        name: "User Authentication System".to_string(),
262        description: "Complete user authentication system with email/password, JWT tokens, and role-based access control.".to_string(),
263        version: "1.2.3".to_string(),
264        author: Some("@ggen-official".to_string()),
265        license: Some("MIT".to_string()),
266        homepage: Some("https://ggen.dev".to_string()),
267        stars: 1245,
268        downloads: 45234,
269        updated_at: "2 days ago".to_string(),
270        tags: vec!["auth".to_string(), "user".to_string(), "jwt".to_string()],
271        health_score: Some(95.0),
272        dependencies: vec![
273            PackageDependency {
274                name: "@ggen/base-entity".to_string(),
275                version: "^2.0.0".to_string(),
276                required: true,
277            },
278            PackageDependency {
279                name: "@ggen/validation-helpers".to_string(),
280                version: "^1.1.0".to_string(),
281                required: true,
282            },
283        ],
284        examples: vec![
285            UsageExample {
286                title: "Basic Installation".to_string(),
287                code: "ggen market install @ggen/auth-user".to_string(),
288                description: "Install the authentication package".to_string(),
289            },
290            UsageExample {
291                title: "Generate Controllers".to_string(),
292                code: "ggen project generate --template @ggen/auth-user/controller".to_string(),
293                description: "Generate authentication controllers".to_string(),
294            },
295        ],
296    }
297}
298
299pub async fn run_with_deps(args: &InfoArgs, fetcher: &dyn GpackMetadataFetcher) -> Result<()> {
300    // Validate input
301    validate_gpack_id(&args.gpack_id)?;
302
303    // Show progress for metadata fetching
304    println!("🔍 Fetching gpack metadata...");
305
306    let metadata = fetcher.fetch_metadata(&args.gpack_id)?;
307
308    println!("📦 Gpack Information:");
309    println!("  ID: {}", metadata.id);
310    println!("  Name: {}", metadata.name);
311    println!("  Description: {}", metadata.description);
312    println!("  Version: {}", metadata.version);
313
314    if let Some(author) = metadata.author {
315        println!("  Author: {}", author);
316    }
317    if let Some(license) = metadata.license {
318        println!("  License: {}", license);
319    }
320    if let Some(homepage) = metadata.homepage {
321        println!("  Homepage: {}", homepage);
322    }
323
324    Ok(())
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use mockall::predicate::*;
331
332    #[tokio::test]
333    async fn test_show_displays_metadata() {
334        let mut mock_fetcher = MockGpackMetadataFetcher::new();
335        mock_fetcher
336            .expect_fetch_metadata()
337            .with(eq(String::from("io.ggen.rust.cli")))
338            .times(1)
339            .returning(|id| {
340                Ok(GpackMetadata {
341                    id: id.to_string(),
342                    name: "Rust CLI".to_string(),
343                    description: "CLI templates for Rust".to_string(),
344                    version: "1.0.0".to_string(),
345                    author: Some("ggen-team".to_string()),
346                    license: Some("MIT".to_string()),
347                    homepage: Some("https://github.com/ggen/rust-cli".to_string()),
348                    stars: 100,
349                    downloads: 1000,
350                    updated_at: "2024-01-01T00:00:00Z".to_string(),
351                    tags: vec!["rust".to_string(), "cli".to_string()],
352                    health_score: Some(0.95),
353                    dependencies: vec![],
354                    examples: vec![],
355                })
356            });
357
358        let args = InfoArgs {
359            gpack_id: "io.ggen.rust.cli".to_string(),
360            examples: false,
361            dependencies: false,
362            health: false,
363            interactive: false,
364        };
365
366        let result = run_with_deps(&args, &mock_fetcher).await;
367        assert!(result.is_ok());
368    }
369}