1use std::sync::Arc;
7
8use chrono::Utc;
9
10use crate::persistence::index::UserMetadataIndex;
11use crate::persistence::types::{
12 AliasExportFile, AliasWithScope, ImportConflictStrategy, ImportResult, SavedAlias,
13 StorageScope, UserMetadata,
14};
15use crate::persistence::validation::{AliasNameError, validate_alias_name};
16
17#[derive(Debug)]
19pub enum AliasError {
20 InvalidName(AliasNameError),
22 NotFound { name: String },
24 AlreadyExists { name: String, scope: StorageScope },
26 Storage(anyhow::Error),
28}
29
30impl std::fmt::Display for AliasError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::InvalidName(e) => write!(f, "invalid alias name: {e}"),
34 Self::NotFound { name } => write!(f, "alias '{name}' not found"),
35 Self::AlreadyExists { name, scope } => {
36 write!(f, "alias '{name}' already exists in {scope} storage")
37 }
38 Self::Storage(e) => write!(f, "storage error: {e}"),
39 }
40 }
41}
42
43impl std::error::Error for AliasError {
44 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
45 match self {
46 Self::InvalidName(e) => Some(e),
47 Self::Storage(e) => e.source(),
48 _ => None,
49 }
50 }
51}
52
53impl From<AliasNameError> for AliasError {
54 fn from(e: AliasNameError) -> Self {
55 Self::InvalidName(e)
56 }
57}
58
59impl From<anyhow::Error> for AliasError {
60 fn from(e: anyhow::Error) -> Self {
61 Self::Storage(e)
62 }
63}
64
65#[derive(Debug, Clone)]
70pub struct AliasManager {
71 index: Arc<UserMetadataIndex>,
72}
73
74impl AliasManager {
75 #[must_use]
77 pub fn new(index: Arc<UserMetadataIndex>) -> Self {
78 Self { index }
79 }
80
81 pub fn save(
98 &self,
99 name: &str,
100 command: &str,
101 args: &[String],
102 description: Option<&str>,
103 scope: StorageScope,
104 ) -> Result<(), AliasError> {
105 validate_alias_name(name)?;
107
108 self.index
110 .update(scope, |metadata| {
111 if metadata.aliases.contains_key(name) {
112 anyhow::bail!("alias '{name}' already exists");
113 }
114
115 let alias = SavedAlias {
116 command: command.to_string(),
117 args: args.to_vec(),
118 created: Utc::now(),
119 description: description.map(String::from),
120 };
121
122 metadata.aliases.insert(name.to_string(), alias);
123 Ok(())
124 })
125 .map_err(|e| {
126 if e.to_string().contains("already exists") {
127 AliasError::AlreadyExists {
128 name: name.to_string(),
129 scope,
130 }
131 } else {
132 AliasError::Storage(e)
133 }
134 })
135 }
136
137 pub fn get(&self, name: &str) -> Result<AliasWithScope, AliasError> {
146 if self.index.has_project_root() {
148 let local = self.index.load(StorageScope::Local)?;
149 if let Some(alias) = local.aliases.get(name) {
150 return Ok(AliasWithScope {
151 name: name.to_string(),
152 alias: alias.clone(),
153 scope: StorageScope::Local,
154 });
155 }
156 }
157
158 let global = self.index.load(StorageScope::Global)?;
160 if let Some(alias) = global.aliases.get(name) {
161 return Ok(AliasWithScope {
162 name: name.to_string(),
163 alias: alias.clone(),
164 scope: StorageScope::Global,
165 });
166 }
167
168 Err(AliasError::NotFound {
169 name: name.to_string(),
170 })
171 }
172
173 pub fn get_from_scope(
179 &self,
180 name: &str,
181 scope: StorageScope,
182 ) -> Result<SavedAlias, AliasError> {
183 let metadata = self.index.load(scope)?;
184 metadata
185 .aliases
186 .get(name)
187 .cloned()
188 .ok_or_else(|| AliasError::NotFound {
189 name: name.to_string(),
190 })
191 }
192
193 pub fn list(&self) -> Result<Vec<AliasWithScope>, AliasError> {
203 let mut result = Vec::new();
204 let mut seen_names = std::collections::HashSet::new();
205
206 if self.index.has_project_root() {
208 let local = self.index.load(StorageScope::Local)?;
209 for (name, alias) in local.aliases {
210 seen_names.insert(name.clone());
211 result.push(AliasWithScope {
212 name,
213 alias,
214 scope: StorageScope::Local,
215 });
216 }
217 }
218
219 let global = self.index.load(StorageScope::Global)?;
221 for (name, alias) in global.aliases {
222 if !seen_names.contains(&name) {
223 result.push(AliasWithScope {
224 name,
225 alias,
226 scope: StorageScope::Global,
227 });
228 }
229 }
230
231 result.sort_by(|a, b| a.name.cmp(&b.name));
233
234 Ok(result)
235 }
236
237 pub fn list_scope(&self, scope: StorageScope) -> Result<Vec<AliasWithScope>, AliasError> {
243 let metadata = self.index.load(scope)?;
244 let mut result: Vec<AliasWithScope> = metadata
245 .aliases
246 .into_iter()
247 .map(|(name, alias)| AliasWithScope { name, alias, scope })
248 .collect();
249
250 result.sort_by(|a, b| a.name.cmp(&b.name));
251 Ok(result)
252 }
253
254 pub fn delete(&self, name: &str, scope: Option<StorageScope>) -> Result<(), AliasError> {
263 let mut deleted = false;
264
265 if let Some(s) = scope {
266 self.delete_in_scope(s, name, &mut deleted)?;
268 } else {
269 if self.index.has_project_root() {
271 self.delete_in_scope(StorageScope::Local, name, &mut deleted)?;
272 }
273
274 self.delete_in_scope(StorageScope::Global, name, &mut deleted)?;
275 }
276
277 if deleted {
278 Ok(())
279 } else {
280 Err(AliasError::NotFound {
281 name: name.to_string(),
282 })
283 }
284 }
285
286 fn delete_in_scope(
287 &self,
288 scope: StorageScope,
289 name: &str,
290 deleted: &mut bool,
291 ) -> Result<(), AliasError> {
292 self.index.update(scope, |metadata| {
293 if metadata.aliases.remove(name).is_some() {
294 *deleted = true;
295 }
296 Ok(())
297 })?;
298 Ok(())
299 }
300
301 pub fn rename(
311 &self,
312 old_name: &str,
313 new_name: &str,
314 scope: Option<StorageScope>,
315 ) -> Result<StorageScope, AliasError> {
316 validate_alias_name(new_name)?;
318
319 let found_scope = self.resolve_alias_scope(old_name, scope)?;
320
321 self.perform_rename(found_scope, old_name, new_name)?;
322
323 Ok(found_scope)
324 }
325
326 fn resolve_alias_scope(
327 &self,
328 old_name: &str,
329 scope: Option<StorageScope>,
330 ) -> Result<StorageScope, AliasError> {
331 if let Some(s) = scope {
332 let metadata = self.index.load(s)?;
333 if metadata.aliases.contains_key(old_name) {
334 return Ok(s);
335 }
336
337 return Err(AliasError::NotFound {
338 name: old_name.to_string(),
339 });
340 }
341
342 let mut found = None;
343 if self.index.has_project_root() {
344 let local = self.index.load(StorageScope::Local)?;
345 if local.aliases.contains_key(old_name) {
346 found = Some(StorageScope::Local);
347 }
348 }
349 if found.is_none() {
350 let global = self.index.load(StorageScope::Global)?;
351 if global.aliases.contains_key(old_name) {
352 found = Some(StorageScope::Global);
353 }
354 }
355
356 found.ok_or_else(|| AliasError::NotFound {
357 name: old_name.to_string(),
358 })
359 }
360
361 fn perform_rename(
362 &self,
363 found_scope: StorageScope,
364 old_name: &str,
365 new_name: &str,
366 ) -> Result<(), AliasError> {
367 self.index
368 .update(found_scope, |metadata| {
369 if metadata.aliases.contains_key(new_name) {
371 anyhow::bail!("alias '{new_name}' already exists");
372 }
373
374 if let Some(alias) = metadata.aliases.remove(old_name) {
376 metadata.aliases.insert(new_name.to_string(), alias);
377 }
378 Ok(())
379 })
380 .map_err(|e| {
381 if e.to_string().contains("already exists") {
382 AliasError::AlreadyExists {
383 name: new_name.to_string(),
384 scope: found_scope,
385 }
386 } else {
387 AliasError::Storage(e)
388 }
389 })?;
390
391 Ok(())
392 }
393
394 fn ensure_no_conflicts(
395 &self,
396 export: &AliasExportFile,
397 scope: StorageScope,
398 ) -> Result<(), AliasError> {
399 let existing = self.index.load(scope)?;
400 for name in export.aliases.keys() {
401 if existing.aliases.contains_key(name) {
402 return Err(AliasError::AlreadyExists {
403 name: name.clone(),
404 scope,
405 });
406 }
407 }
408 Ok(())
409 }
410
411 fn apply_import_entry(
412 metadata: &mut UserMetadata,
413 name: &str,
414 alias: &SavedAlias,
415 strategy: ImportConflictStrategy,
416 result: &mut ImportResult,
417 ) {
418 if metadata.aliases.contains_key(name) {
419 match strategy {
420 ImportConflictStrategy::Skip => {
421 result.skipped += 1;
422 result.skipped_names.push(name.to_string());
423 }
424 ImportConflictStrategy::Overwrite => {
425 metadata.aliases.insert(name.to_string(), alias.clone());
426 result.overwritten += 1;
427 }
428 ImportConflictStrategy::Fail => {
429 unreachable!();
431 }
432 }
433 } else {
434 metadata.aliases.insert(name.to_string(), alias.clone());
435 result.imported += 1;
436 }
437 }
438
439 #[must_use]
443 pub fn exists(&self, name: &str) -> bool {
444 self.get(name).is_ok()
445 }
446
447 pub fn count(&self) -> Result<(usize, usize), AliasError> {
453 let local_count = if self.index.has_project_root() {
454 self.index.load(StorageScope::Local)?.aliases.len()
455 } else {
456 0
457 };
458 let global_count = self.index.load(StorageScope::Global)?.aliases.len();
459 Ok((local_count, global_count))
460 }
461
462 pub fn import(
468 &self,
469 export: &AliasExportFile,
470 scope: StorageScope,
471 strategy: ImportConflictStrategy,
472 ) -> Result<ImportResult, AliasError> {
473 let mut result = ImportResult {
474 imported: 0,
475 skipped: 0,
476 failed: 0,
477 overwritten: 0,
478 skipped_names: Vec::new(),
479 };
480
481 if strategy == ImportConflictStrategy::Fail {
483 self.ensure_no_conflicts(export, scope)?;
484 }
485
486 self.index.update(scope, |metadata| {
488 for (name, alias) in &export.aliases {
489 Self::apply_import_entry(metadata, name, alias, strategy, &mut result);
490 }
491 Ok(())
492 })?;
493
494 Ok(result)
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::persistence::config::PersistenceConfig;
502 use tempfile::TempDir;
503
504 fn setup() -> (TempDir, Arc<UserMetadataIndex>) {
505 let dir = TempDir::new().unwrap();
506 let config = PersistenceConfig {
507 global_dir_override: Some(dir.path().join("global")),
508 ..Default::default()
509 };
510 let index = Arc::new(UserMetadataIndex::open(Some(dir.path()), config).unwrap());
511 (dir, index)
512 }
513
514 #[test]
515 fn test_save_and_get_alias() {
516 let (_dir, index) = setup();
517 let manager = AliasManager::new(index);
518
519 manager
520 .save(
521 "test-query",
522 "search",
523 &[
524 "main".to_string(),
525 "--kind".to_string(),
526 "function".to_string(),
527 ],
528 Some("Find main functions"),
529 StorageScope::Global,
530 )
531 .unwrap();
532
533 let alias = manager.get("test-query").unwrap();
534 assert_eq!(alias.name, "test-query");
535 assert_eq!(alias.alias.command, "search");
536 assert_eq!(alias.alias.args, vec!["main", "--kind", "function"]);
537 assert_eq!(
538 alias.alias.description,
539 Some("Find main functions".to_string())
540 );
541 assert_eq!(alias.scope, StorageScope::Global);
542 }
543
544 #[test]
545 fn test_local_takes_precedence() {
546 let (_dir, index) = setup();
547 let manager = AliasManager::new(index);
548
549 manager
551 .save(
552 "shared",
553 "search",
554 &["global".to_string()],
555 None,
556 StorageScope::Global,
557 )
558 .unwrap();
559
560 manager
562 .save(
563 "shared",
564 "query",
565 &["local".to_string()],
566 None,
567 StorageScope::Local,
568 )
569 .unwrap();
570
571 let alias = manager.get("shared").unwrap();
573 assert_eq!(alias.alias.command, "query");
574 assert_eq!(alias.alias.args, vec!["local"]);
575 assert_eq!(alias.scope, StorageScope::Local);
576 }
577
578 #[test]
579 fn test_list_aliases() {
580 let (_dir, index) = setup();
581 let manager = AliasManager::new(index);
582
583 manager
584 .save("alpha", "search", &[], None, StorageScope::Global)
585 .unwrap();
586 manager
587 .save("beta", "query", &[], None, StorageScope::Local)
588 .unwrap();
589 manager
590 .save("gamma", "search", &[], None, StorageScope::Global)
591 .unwrap();
592
593 let list = manager.list().unwrap();
594 assert_eq!(list.len(), 3);
595 assert_eq!(list[0].name, "alpha");
596 assert_eq!(list[1].name, "beta");
597 assert_eq!(list[2].name, "gamma");
598 }
599
600 #[test]
601 fn test_delete_alias() {
602 let (_dir, index) = setup();
603 let manager = AliasManager::new(index);
604
605 manager
606 .save("to-delete", "search", &[], None, StorageScope::Global)
607 .unwrap();
608
609 assert!(manager.exists("to-delete"));
610
611 manager.delete("to-delete", None).unwrap();
612
613 assert!(!manager.exists("to-delete"));
614 }
615
616 #[test]
617 fn test_delete_from_specific_scope() {
618 let (_dir, index) = setup();
619 let manager = AliasManager::new(index);
620
621 manager
623 .save(
624 "shared",
625 "search",
626 &["global".to_string()],
627 None,
628 StorageScope::Global,
629 )
630 .unwrap();
631 manager
632 .save(
633 "shared",
634 "query",
635 &["local".to_string()],
636 None,
637 StorageScope::Local,
638 )
639 .unwrap();
640
641 manager.delete("shared", Some(StorageScope::Local)).unwrap();
643
644 let alias = manager.get("shared").unwrap();
646 assert_eq!(alias.scope, StorageScope::Global);
647 assert_eq!(alias.alias.args, vec!["global"]);
648 }
649
650 #[test]
651 fn test_rename_alias() {
652 let (_dir, index) = setup();
653 let manager = AliasManager::new(index);
654
655 manager
656 .save(
657 "old-name",
658 "search",
659 &["test".to_string()],
660 None,
661 StorageScope::Global,
662 )
663 .unwrap();
664
665 let scope = manager.rename("old-name", "new-name", None).unwrap();
666 assert_eq!(scope, StorageScope::Global);
667
668 assert!(!manager.exists("old-name"));
669 assert!(manager.exists("new-name"));
670
671 let alias = manager.get("new-name").unwrap();
672 assert_eq!(alias.alias.args, vec!["test"]);
673 }
674
675 #[test]
676 fn test_rename_to_existing_fails() {
677 let (_dir, index) = setup();
678 let manager = AliasManager::new(index);
679
680 manager
681 .save("first", "search", &[], None, StorageScope::Global)
682 .unwrap();
683 manager
684 .save("second", "query", &[], None, StorageScope::Global)
685 .unwrap();
686
687 let result = manager.rename("first", "second", None);
688 assert!(matches!(result, Err(AliasError::AlreadyExists { .. })));
689 }
690
691 #[test]
692 fn test_save_invalid_name_fails() {
693 let (_dir, index) = setup();
694 let manager = AliasManager::new(index);
695
696 let result = manager.save("123invalid", "search", &[], None, StorageScope::Global);
697 assert!(matches!(result, Err(AliasError::InvalidName(_))));
698 }
699
700 #[test]
701 fn test_get_nonexistent_fails() {
702 let (_dir, index) = setup();
703 let manager = AliasManager::new(index);
704
705 let result = manager.get("nonexistent");
706 assert!(matches!(result, Err(AliasError::NotFound { .. })));
707 }
708
709 #[test]
710 fn test_duplicate_save_fails() {
711 let (_dir, index) = setup();
712 let manager = AliasManager::new(index);
713
714 manager
715 .save("unique", "search", &[], None, StorageScope::Global)
716 .unwrap();
717
718 let result = manager.save("unique", "query", &[], None, StorageScope::Global);
719 assert!(matches!(result, Err(AliasError::AlreadyExists { .. })));
720 }
721
722 #[test]
723 fn test_count() {
724 let (_dir, index) = setup();
725 let manager = AliasManager::new(index);
726
727 assert_eq!(manager.count().unwrap(), (0, 0));
728
729 manager
730 .save("global1", "search", &[], None, StorageScope::Global)
731 .unwrap();
732 manager
733 .save("global2", "search", &[], None, StorageScope::Global)
734 .unwrap();
735 manager
736 .save("local1", "search", &[], None, StorageScope::Local)
737 .unwrap();
738
739 assert_eq!(manager.count().unwrap(), (1, 2));
740 }
741
742 #[test]
743 fn test_list_scope() {
744 let (_dir, index) = setup();
745 let manager = AliasManager::new(index);
746
747 manager
748 .save("global1", "search", &[], None, StorageScope::Global)
749 .unwrap();
750 manager
751 .save("local1", "query", &[], None, StorageScope::Local)
752 .unwrap();
753
754 let global_list = manager.list_scope(StorageScope::Global).unwrap();
755 assert_eq!(global_list.len(), 1);
756 assert_eq!(global_list[0].name, "global1");
757
758 let local_list = manager.list_scope(StorageScope::Local).unwrap();
759 assert_eq!(local_list.len(), 1);
760 assert_eq!(local_list[0].name, "local1");
761 }
762
763 #[test]
764 fn test_error_display() {
765 let err = AliasError::NotFound {
766 name: "test".to_string(),
767 };
768 assert_eq!(err.to_string(), "alias 'test' not found");
769
770 let err = AliasError::AlreadyExists {
771 name: "test".to_string(),
772 scope: StorageScope::Global,
773 };
774 assert_eq!(
775 err.to_string(),
776 "alias 'test' already exists in global storage"
777 );
778 }
779}