apicurio_cli/
identifier.rs1use anyhow::{anyhow, Result};
2use dialoguer::{Input, Select};
3use fuzzy_matcher::skim::SkimMatcherV2;
4use fuzzy_matcher::FuzzyMatcher;
5
6#[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 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 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 let parts: Vec<&str> = main_part.split('/').collect();
39
40 match parts.len() {
41 1 if !parts[0].is_empty() => {
42 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 }
67 }
68
69 identifier
70 }
71
72 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 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 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 if self.group_id.is_none() {
111 let available_group_ids: Vec<String>;
112
113 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 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 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 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) .interact()?;
149
150 if selection == options.len() - 1 {
151 self.group_id = Some(
153 Input::new()
154 .with_prompt("Enter custom group ID")
155 .interact_text()?,
156 );
157 } else {
158 self.group_id = Some(available_group_ids[selection].clone());
160 }
161 } else {
162 self.group_id = Some("default".to_string());
164 println!("âšī¸ No groups found, using default group: 'default'");
165 }
166 }
167
168 if self.artifact_id.is_none() {
170 let available_artifacts: Vec<String>;
171
172 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 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 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 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) .interact()?;
216
217 if selection == options.len() - 1 {
218 self.artifact_id = Some(
220 Input::new()
221 .with_prompt("Enter custom artifact ID")
222 .interact_text()?,
223 );
224 } else {
225 self.artifact_id = Some(available_artifacts[selection].clone());
227 }
228 } else {
229 self.artifact_id = Some(Input::new().with_prompt("Artifact ID").interact_text()?);
231 }
232 }
233
234 Ok(())
238 }
239
240 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(()); }
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 match registry_client.list_versions(group_id, artifact_id).await {
261 Ok(mut versions) => {
262 if versions.is_empty() {
263 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 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 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) .interact()?;
285
286 if selection == options.len() - 1 {
287 self.version = Some(
289 Input::new()
290 .with_prompt("Enter custom version (semver)")
291 .interact_text()?,
292 );
293 } else {
294 self.version = Some(version_strings[selection].clone());
296 }
297 }
298 }
299 Err(_) => {
300 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 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 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 if let Some(registry) = &self.registry {
340 if dep.registry == *registry {
341 score += 100;
342 } else {
343 is_match = false;
344 }
345 }
346
347 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 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 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; }
381 }
382
383 if is_match && score > 0 {
384 matches.push(dep);
385 }
386 }
387
388 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 });
415
416 matches
417 }
418
419 #[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()); }
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()); }
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 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}