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.group_id.clone())
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.group_id.clone())
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.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 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 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) .interact()?;
214
215 if selection == options.len() - 1 {
216 self.artifact_id = Some(
218 Input::new()
219 .with_prompt("Enter custom artifact ID")
220 .interact_text()?,
221 );
222 } else {
223 self.artifact_id = Some(available_artifacts[selection].clone());
225 }
226 } else {
227 self.artifact_id = Some(Input::new().with_prompt("Artifact ID").interact_text()?);
229 }
230 }
231
232 Ok(())
236 }
237
238 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(()); }
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 match registry_client.list_versions(group_id, artifact_id).await {
259 Ok(mut versions) => {
260 if versions.is_empty() {
261 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 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 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) .interact()?;
283
284 if selection == options.len() - 1 {
285 self.version = Some(
287 Input::new()
288 .with_prompt("Enter custom version (semver)")
289 .interact_text()?,
290 );
291 } else {
292 self.version = Some(version_strings[selection].clone());
294 }
295 }
296 }
297 Err(_) => {
298 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 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 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 if let Some(registry) = &self.registry {
338 if dep.registry == *registry {
339 score += 100;
340 } else {
341 is_match = false;
342 }
343 }
344
345 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 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 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; }
375 }
376
377 if is_match && score > 0 {
378 matches.push(dep);
379 }
380 }
381
382 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 });
409
410 matches
411 }
412
413 #[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()); }
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()); }
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 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}