Skip to main content

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.resolved_group_id())
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.resolved_group_id())
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.resolved_group_id() == *group_id)
184                                .map(|d| d.resolved_artifact_id())
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| {
198                        d.resolved_group_id() == *self.group_id.as_ref().unwrap_or(&String::new())
199                    })
200                    .map(|d| d.resolved_artifact_id())
201                    .collect::<std::collections::HashSet<_>>()
202                    .into_iter()
203                    .collect();
204            }
205
206            if !available_artifacts.is_empty() {
207                // Show available artifacts and allow selection or custom input
208                let mut options = available_artifacts.clone();
209                options.push("📝 Enter custom artifact ID".to_string());
210
211                let selection = Select::new()
212                    .with_prompt("Artifact ID")
213                    .items(&options)
214                    .default(options.len() - 1) // Default to custom input
215                    .interact()?;
216
217                if selection == options.len() - 1 {
218                    // User chose to enter custom artifact ID
219                    self.artifact_id = Some(
220                        Input::new()
221                            .with_prompt("Enter custom artifact ID")
222                            .interact_text()?,
223                    );
224                } else {
225                    // User selected an available artifact ID
226                    self.artifact_id = Some(available_artifacts[selection].clone());
227                }
228            } else {
229                // No available artifacts, just prompt for input
230                self.artifact_id = Some(Input::new().with_prompt("Artifact ID").interact_text()?);
231            }
232        }
233
234        // Complete version - we'll handle this separately after we have registry access
235        // This will be completed later in the add command
236
237        Ok(())
238    }
239
240    /// Complete the version field by fetching available versions from the registry
241    pub async fn complete_version_with_registry(
242        &mut self,
243        registry_client: &crate::registry::RegistryClient,
244    ) -> Result<()> {
245        if self.version.is_some() {
246            return Ok(()); // Version already specified
247        }
248
249        let group_id = self
250            .group_id
251            .as_ref()
252            .ok_or_else(|| anyhow!("Group ID must be specified before completing version"))?;
253
254        let artifact_id = self
255            .artifact_id
256            .as_ref()
257            .ok_or_else(|| anyhow!("Artifact ID must be specified before completing version"))?;
258
259        // Fetch available versions from the registry
260        match registry_client.list_versions(group_id, artifact_id).await {
261            Ok(mut versions) => {
262                if versions.is_empty() {
263                    // No existing versions, prompt user with default
264                    self.version = Some(
265                        Input::new()
266                            .with_prompt("Version (semver)")
267                            .default("1.0.0".to_string())
268                            .interact_text()?,
269                    );
270                } else {
271                    // Sort versions in descending order to get the latest first
272                    versions.sort_by(|a, b| b.cmp(a));
273                    let version_strings: Vec<String> =
274                        versions.iter().map(|v| v.to_string()).collect();
275
276                    // Create options for the select menu
277                    let mut options = version_strings.clone();
278                    options.push("📝 Enter custom version".to_string());
279
280                    let selection = Select::new()
281                        .with_prompt(format!("Select version for {group_id}/{artifact_id}"))
282                        .items(&options)
283                        .default(0) // Default to latest version
284                        .interact()?;
285
286                    if selection == options.len() - 1 {
287                        // User chose to enter custom version
288                        self.version = Some(
289                            Input::new()
290                                .with_prompt("Enter custom version (semver)")
291                                .interact_text()?,
292                        );
293                    } else {
294                        // User selected an existing version
295                        self.version = Some(version_strings[selection].clone());
296                    }
297                }
298            }
299            Err(_) => {
300                // Registry query failed (artifact might not exist yet), use default
301                println!("â„šī¸ Could not fetch existing versions (artifact may not exist yet)");
302                self.version = Some(
303                    Input::new()
304                        .with_prompt("Version (semver)")
305                        .default("1.0.0".to_string())
306                        .interact_text()?,
307                );
308            }
309        }
310
311        Ok(())
312    }
313
314    /// Validate that the artifact exists in the registry
315    pub async fn validate_artifact_exists(
316        &self,
317        registry_client: &crate::registry::RegistryClient,
318    ) -> Result<bool> {
319        if let (Some(group_id), Some(artifact_id)) = (&self.group_id, &self.artifact_id) {
320            registry_client.artifact_exists(group_id, artifact_id).await
321        } else {
322            Ok(false)
323        }
324    }
325
326    /// Find dependencies that match this identifier (for removal)
327    pub fn find_matches<'a>(
328        &self,
329        dependencies: &'a [crate::config::DependencyConfig],
330    ) -> Vec<&'a crate::config::DependencyConfig> {
331        let matcher = SkimMatcherV2::default();
332        let mut matches = Vec::new();
333
334        for dep in dependencies {
335            let mut score = 0i64;
336            let mut is_match = true;
337
338            // Check registry match
339            if let Some(registry) = &self.registry {
340                if dep.registry == *registry {
341                    score += 100;
342                } else {
343                    is_match = false;
344                }
345            }
346
347            // Check group_id match
348            if let Some(group_id) = &self.group_id {
349                let resolved_group_id = dep.resolved_group_id();
350                if resolved_group_id == *group_id {
351                    score += 100;
352                } else if let Some(fuzzy_score) = matcher.fuzzy_match(&resolved_group_id, group_id)
353                {
354                    score += fuzzy_score;
355                } else {
356                    is_match = false;
357                }
358            }
359
360            // Check artifact_id match
361            if let Some(artifact_id) = &self.artifact_id {
362                let resolved_artifact_id = dep.resolved_artifact_id();
363                if resolved_artifact_id == *artifact_id {
364                    score += 100;
365                } else if let Some(fuzzy_score) =
366                    matcher.fuzzy_match(&resolved_artifact_id, artifact_id)
367                {
368                    score += fuzzy_score;
369                } else {
370                    is_match = false;
371                }
372            }
373
374            // Check version match (exact or fuzzy)
375            if let Some(version) = &self.version {
376                if dep.version == *version {
377                    score += 50;
378                } else if let Some(fuzzy_score) = matcher.fuzzy_match(&dep.version, version) {
379                    score += fuzzy_score / 2; // Lower weight for version fuzzy match
380                }
381            }
382
383            if is_match && score > 0 {
384                matches.push(dep);
385            }
386        }
387
388        // Sort by best match first
389        matches.sort_by_key(|dep| {
390            let mut score = 0i64;
391
392            if let Some(registry) = &self.registry {
393                if dep.registry == *registry {
394                    score += 100;
395                }
396            }
397            if let Some(group_id) = &self.group_id {
398                if dep.resolved_group_id() == *group_id {
399                    score += 100;
400                }
401            }
402            if let Some(artifact_id) = &self.artifact_id {
403                if dep.resolved_artifact_id() == *artifact_id {
404                    score += 100;
405                }
406            }
407            if let Some(version) = &self.version {
408                if dep.version == *version {
409                    score += 50;
410                }
411            }
412
413            -score // Negative for descending sort
414        });
415
416        matches
417    }
418
419    /// Convert to a display string
420    #[allow(dead_code)]
421    pub fn to_display_string(&self) -> String {
422        let mut parts = Vec::new();
423
424        if let Some(registry) = &self.registry {
425            parts.push(registry.clone());
426        }
427        if let Some(group_id) = &self.group_id {
428            if parts.is_empty() {
429                parts.push(String::new()); // placeholder for missing registry
430            }
431            parts.push(group_id.clone());
432        }
433        if let Some(artifact_id) = &self.artifact_id {
434            while parts.len() < 2 {
435                parts.push(String::new()); // placeholders
436            }
437            parts.push(artifact_id.clone());
438        }
439
440        let main_part = parts.join("/");
441
442        if let Some(version) = &self.version {
443            format!("{main_part}@{version}")
444        } else {
445            main_part
446        }
447    }
448
449    /// Check if all required fields are present
450    pub fn is_complete(&self) -> bool {
451        self.registry.is_some()
452            && self.group_id.is_some()
453            && self.artifact_id.is_some()
454            && self.version.is_some()
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_parse_full_identifier() {
464        let id = Identifier::parse("myregistry/com.example/myartifact@1.0.0");
465        assert_eq!(id.registry, Some("myregistry".to_string()));
466        assert_eq!(id.group_id, Some("com.example".to_string()));
467        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
468        assert_eq!(id.version, Some("1.0.0".to_string()));
469    }
470
471    #[test]
472    fn test_parse_partial_identifier() {
473        let id = Identifier::parse("com.example/myartifact");
474        assert_eq!(id.registry, None);
475        assert_eq!(id.group_id, Some("com.example".to_string()));
476        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
477        assert_eq!(id.version, None);
478    }
479
480    #[test]
481    fn test_parse_artifact_only() {
482        let id = Identifier::parse("myartifact@2.0.0");
483        assert_eq!(id.registry, None);
484        assert_eq!(id.group_id, None);
485        assert_eq!(id.artifact_id, Some("myartifact".to_string()));
486        assert_eq!(id.version, Some("2.0.0".to_string()));
487    }
488
489    #[test]
490    fn test_display_string() {
491        let mut id = Identifier::parse("myregistry/com.example/myartifact@1.0.0");
492        assert_eq!(
493            id.to_display_string(),
494            "myregistry/com.example/myartifact@1.0.0"
495        );
496
497        id.version = None;
498        assert_eq!(id.to_display_string(), "myregistry/com.example/myartifact");
499    }
500}