apicurio_cli/
identifier.rs

1use anyhow::{anyhow, Result};
2use dialoguer::{Input, Select};
3use fuzzy_matcher::skim::SkimMatcherV2;
4use fuzzy_matcher::FuzzyMatcher;
5
6/// Represents a parsed identifier in the format registry/group_id/artifact_id@version
7#[derive(Debug, Clone)]
8pub struct Identifier {
9    pub registry: Option<String>,
10    pub group_id: Option<String>,
11    pub artifact_id: Option<String>,
12    pub version: Option<String>,
13}
14
15impl Identifier {
16    /// Parse an identifier string in the format registry/group_id/artifact_id@version
17    /// All parts are optional
18    pub fn parse(input: &str) -> Self {
19        let mut identifier = Identifier {
20            registry: None,
21            group_id: None,
22            artifact_id: None,
23            version: None,
24        };
25
26        // Split by @ to separate version
27        let main_part = if let Some(at_pos) = input.rfind('@') {
28            let version = &input[at_pos + 1..];
29            if !version.is_empty() {
30                identifier.version = Some(version.to_string());
31            }
32            &input[..at_pos]
33        } else {
34            input
35        };
36
37        // Split by / to get registry/group_id/artifact_id
38        let parts: Vec<&str> = main_part.split('/').collect();
39
40        match parts.len() {
41            1 if !parts[0].is_empty() => {
42                // Could be any of the three parts, we'll prompt for clarification
43                identifier.artifact_id = Some(parts[0].to_string());
44            }
45            2 => {
46                if !parts[0].is_empty() {
47                    identifier.group_id = Some(parts[0].to_string());
48                }
49                if !parts[1].is_empty() {
50                    identifier.artifact_id = Some(parts[1].to_string());
51                }
52            }
53            3 => {
54                if !parts[0].is_empty() {
55                    identifier.registry = Some(parts[0].to_string());
56                }
57                if !parts[1].is_empty() {
58                    identifier.group_id = Some(parts[1].to_string());
59                }
60                if !parts[2].is_empty() {
61                    identifier.artifact_id = Some(parts[2].to_string());
62                }
63            }
64            _ => {
65                // Empty or too many parts, will be handled during completion
66            }
67        }
68
69        identifier
70    }
71
72    /// Complete missing fields by prompting the user with available options
73    pub async fn complete_interactive(
74        &mut self,
75        available_registries: &[String],
76        existing_dependencies: &[crate::config::DependencyConfig],
77        registry_client: Option<&crate::registry::RegistryClient>,
78    ) -> Result<()> {
79        // Complete registry
80        if self.registry.is_none() {
81            if available_registries.is_empty() {
82                return Err(anyhow!(
83                    "No registries available. Please configure a registry first."
84                ));
85            }
86
87            if available_registries.len() == 1 {
88                self.registry = Some(available_registries[0].clone());
89                println!("Using registry: {}", available_registries[0]);
90            } else {
91                let selection = Select::new()
92                    .with_prompt("Select registry")
93                    .items(available_registries)
94                    .default(0)
95                    .interact()?;
96                self.registry = Some(available_registries[selection].clone());
97            }
98        } else {
99            // Validate the provided registry
100            if !available_registries.contains(self.registry.as_ref().unwrap()) {
101                return Err(anyhow!(
102                    "Registry '{}' not found. Available registries: {}",
103                    self.registry.as_ref().unwrap(),
104                    available_registries.join(", ")
105                ));
106            }
107        }
108
109        // Complete group_id
110        if self.group_id.is_none() {
111            let available_group_ids: Vec<String>;
112
113            // Try to fetch groups from the registry if available
114            if let Some(client) = registry_client {
115                match client.list_groups().await {
116                    Ok(registry_groups) => {
117                        available_group_ids = registry_groups;
118                    }
119                    Err(_) => {
120                        // Fall back to existing dependencies if registry query fails
121                        available_group_ids = existing_dependencies
122                            .iter()
123                            .map(|d| d.group_id.clone())
124                            .collect::<std::collections::HashSet<_>>()
125                            .into_iter()
126                            .collect();
127                    }
128                }
129            } else {
130                // No registry client, use existing dependencies
131                available_group_ids = existing_dependencies
132                    .iter()
133                    .map(|d| d.group_id.clone())
134                    .collect::<std::collections::HashSet<_>>()
135                    .into_iter()
136                    .collect();
137            }
138
139            if !available_group_ids.is_empty() {
140                // Show available group IDs and allow selection or custom input
141                let mut options = available_group_ids.clone();
142                options.push("📝 Enter custom group ID".to_string());
143
144                let selection = Select::new()
145                    .with_prompt("Group ID")
146                    .items(&options)
147                    .default(options.len() - 1) // Default to custom input
148                    .interact()?;
149
150                if selection == options.len() - 1 {
151                    // User chose to enter custom group ID
152                    self.group_id = Some(
153                        Input::new()
154                            .with_prompt("Enter custom group ID")
155                            .interact_text()?,
156                    );
157                } else {
158                    // User selected an available group ID
159                    self.group_id = Some(available_group_ids[selection].clone());
160                }
161            } else {
162                // No available group IDs, default to "default"
163                self.group_id = Some("default".to_string());
164                println!("â„šī¸ No groups found, using default group: 'default'");
165            }
166        }
167
168        // Complete artifact_id
169        if self.artifact_id.is_none() {
170            let available_artifacts: Vec<String>;
171
172            // Try to fetch artifacts from the registry if available
173            if let Some(client) = registry_client {
174                if let Some(group_id) = &self.group_id {
175                    match client.list_artifacts(group_id).await {
176                        Ok(registry_artifacts) => {
177                            available_artifacts = registry_artifacts;
178                        }
179                        Err(_) => {
180                            // Fall back to existing dependencies if registry query fails
181                            available_artifacts = existing_dependencies
182                                .iter()
183                                .filter(|d| d.group_id == *group_id)
184                                .map(|d| d.artifact_id.clone())
185                                .collect::<std::collections::HashSet<_>>()
186                                .into_iter()
187                                .collect();
188                        }
189                    }
190                } else {
191                    available_artifacts = Vec::new();
192                }
193            } else {
194                // No registry client, use existing dependencies
195                available_artifacts = existing_dependencies
196                    .iter()
197                    .filter(|d| d.group_id == *self.group_id.as_ref().unwrap_or(&String::new()))
198                    .map(|d| d.artifact_id.clone())
199                    .collect::<std::collections::HashSet<_>>()
200                    .into_iter()
201                    .collect();
202            }
203
204            if !available_artifacts.is_empty() {
205                // Show available artifacts and allow selection or custom input
206                let mut options = available_artifacts.clone();
207                options.push("📝 Enter custom artifact ID".to_string());
208
209                let selection = Select::new()
210                    .with_prompt("Artifact ID")
211                    .items(&options)
212                    .default(options.len() - 1) // Default to custom input
213                    .interact()?;
214
215                if selection == options.len() - 1 {
216                    // User chose to enter custom artifact ID
217                    self.artifact_id = Some(
218                        Input::new()
219                            .with_prompt("Enter custom artifact ID")
220                            .interact_text()?,
221                    );
222                } else {
223                    // User selected an available artifact ID
224                    self.artifact_id = Some(available_artifacts[selection].clone());
225                }
226            } else {
227                // No available artifacts, just prompt for input
228                self.artifact_id = Some(Input::new().with_prompt("Artifact ID").interact_text()?);
229            }
230        }
231
232        // Complete version - we'll handle this separately after we have registry access
233        // This will be completed later in the add command
234
235        Ok(())
236    }
237
238    /// Complete the version field by fetching available versions from the registry
239    pub async fn complete_version_with_registry(
240        &mut self,
241        registry_client: &crate::registry::RegistryClient,
242    ) -> Result<()> {
243        if self.version.is_some() {
244            return Ok(()); // Version already specified
245        }
246
247        let group_id = self
248            .group_id
249            .as_ref()
250            .ok_or_else(|| anyhow!("Group ID must be specified before completing version"))?;
251
252        let artifact_id = self
253            .artifact_id
254            .as_ref()
255            .ok_or_else(|| anyhow!("Artifact ID must be specified before completing version"))?;
256
257        // Fetch available versions from the registry
258        match registry_client.list_versions(group_id, artifact_id).await {
259            Ok(mut versions) => {
260                if versions.is_empty() {
261                    // No existing versions, prompt user with default
262                    self.version = Some(
263                        Input::new()
264                            .with_prompt("Version (semver)")
265                            .default("1.0.0".to_string())
266                            .interact_text()?,
267                    );
268                } else {
269                    // Sort versions in descending order to get the latest first
270                    versions.sort_by(|a, b| b.cmp(a));
271                    let version_strings: Vec<String> =
272                        versions.iter().map(|v| v.to_string()).collect();
273
274                    // Create options for the select menu
275                    let mut options = version_strings.clone();
276                    options.push("📝 Enter custom version".to_string());
277
278                    let selection = Select::new()
279                        .with_prompt(format!("Select version for {group_id}/{artifact_id}"))
280                        .items(&options)
281                        .default(0) // Default to latest version
282                        .interact()?;
283
284                    if selection == options.len() - 1 {
285                        // User chose to enter custom version
286                        self.version = Some(
287                            Input::new()
288                                .with_prompt("Enter custom version (semver)")
289                                .interact_text()?,
290                        );
291                    } else {
292                        // User selected an existing version
293                        self.version = Some(version_strings[selection].clone());
294                    }
295                }
296            }
297            Err(_) => {
298                // Registry query failed (artifact might not exist yet), use default
299                println!("â„šī¸ Could not fetch existing versions (artifact may not exist yet)");
300                self.version = Some(
301                    Input::new()
302                        .with_prompt("Version (semver)")
303                        .default("1.0.0".to_string())
304                        .interact_text()?,
305                );
306            }
307        }
308
309        Ok(())
310    }
311
312    /// Validate that the artifact exists in the registry
313    pub async fn validate_artifact_exists(
314        &self,
315        registry_client: &crate::registry::RegistryClient,
316    ) -> Result<bool> {
317        if let (Some(group_id), Some(artifact_id)) = (&self.group_id, &self.artifact_id) {
318            registry_client.artifact_exists(group_id, artifact_id).await
319        } else {
320            Ok(false)
321        }
322    }
323
324    /// Find dependencies that match this identifier (for removal)
325    pub fn find_matches<'a>(
326        &self,
327        dependencies: &'a [crate::config::DependencyConfig],
328    ) -> Vec<&'a crate::config::DependencyConfig> {
329        let matcher = SkimMatcherV2::default();
330        let mut matches = Vec::new();
331
332        for dep in dependencies {
333            let mut score = 0i64;
334            let mut is_match = true;
335
336            // Check registry match
337            if let Some(registry) = &self.registry {
338                if dep.registry == *registry {
339                    score += 100;
340                } else {
341                    is_match = false;
342                }
343            }
344
345            // Check group_id match
346            if let Some(group_id) = &self.group_id {
347                if dep.group_id == *group_id {
348                    score += 100;
349                } else if let Some(fuzzy_score) = matcher.fuzzy_match(&dep.group_id, group_id) {
350                    score += fuzzy_score;
351                } else {
352                    is_match = false;
353                }
354            }
355
356            // Check artifact_id match
357            if let Some(artifact_id) = &self.artifact_id {
358                if dep.artifact_id == *artifact_id {
359                    score += 100;
360                } else if let Some(fuzzy_score) = matcher.fuzzy_match(&dep.artifact_id, artifact_id)
361                {
362                    score += fuzzy_score;
363                } else {
364                    is_match = false;
365                }
366            }
367
368            // Check version match (exact or fuzzy)
369            if let Some(version) = &self.version {
370                if dep.version == *version {
371                    score += 50;
372                } else if let Some(fuzzy_score) = matcher.fuzzy_match(&dep.version, version) {
373                    score += fuzzy_score / 2; // Lower weight for version fuzzy match
374                }
375            }
376
377            if is_match && score > 0 {
378                matches.push(dep);
379            }
380        }
381
382        // Sort by best match first
383        matches.sort_by_key(|dep| {
384            let mut score = 0i64;
385
386            if let Some(registry) = &self.registry {
387                if dep.registry == *registry {
388                    score += 100;
389                }
390            }
391            if let Some(group_id) = &self.group_id {
392                if dep.group_id == *group_id {
393                    score += 100;
394                }
395            }
396            if let Some(artifact_id) = &self.artifact_id {
397                if dep.artifact_id == *artifact_id {
398                    score += 100;
399                }
400            }
401            if let Some(version) = &self.version {
402                if dep.version == *version {
403                    score += 50;
404                }
405            }
406
407            -score // Negative for descending sort
408        });
409
410        matches
411    }
412
413    /// Convert to a display string
414    #[allow(dead_code)]
415    pub fn to_display_string(&self) -> String {
416        let mut parts = Vec::new();
417
418        if let Some(registry) = &self.registry {
419            parts.push(registry.clone());
420        }
421        if let Some(group_id) = &self.group_id {
422            if parts.is_empty() {
423                parts.push(String::new()); // placeholder for missing registry
424            }
425            parts.push(group_id.clone());
426        }
427        if let Some(artifact_id) = &self.artifact_id {
428            while parts.len() < 2 {
429                parts.push(String::new()); // placeholders
430            }
431            parts.push(artifact_id.clone());
432        }
433
434        let main_part = parts.join("/");
435
436        if let Some(version) = &self.version {
437            format!("{main_part}@{version}")
438        } else {
439            main_part
440        }
441    }
442
443    /// Check if all required fields are present
444    pub fn is_complete(&self) -> bool {
445        self.registry.is_some()
446            && self.group_id.is_some()
447            && self.artifact_id.is_some()
448            && self.version.is_some()
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_parse_full_identifier() {
458        let id = Identifier::parse("myregistry/com.example/myartifact@1.0.0");
459        assert_eq!(id.registry, Some("myregistry".to_string()));
460        assert_eq!(id.group_id, Some("com.example".to_string()));
461        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
462        assert_eq!(id.version, Some("1.0.0".to_string()));
463    }
464
465    #[test]
466    fn test_parse_partial_identifier() {
467        let id = Identifier::parse("com.example/myartifact");
468        assert_eq!(id.registry, None);
469        assert_eq!(id.group_id, Some("com.example".to_string()));
470        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
471        assert_eq!(id.version, None);
472    }
473
474    #[test]
475    fn test_parse_artifact_only() {
476        let id = Identifier::parse("myartifact@2.0.0");
477        assert_eq!(id.registry, None);
478        assert_eq!(id.group_id, None);
479        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
480        assert_eq!(id.version, Some("2.0.0".to_string()));
481    }
482
483    #[test]
484    fn test_display_string() {
485        let mut id = Identifier::parse("myregistry/com.example/myartifact@1.0.0");
486        assert_eq!(
487            id.to_display_string(),
488            "myregistry/com.example/myartifact@1.0.0"
489        );
490
491        id.version = None;
492        assert_eq!(id.to_display_string(), "myregistry/com.example/myartifact");
493    }
494}