soroban_cli/commands/contract/alias/
ls.rs1use clap::Parser;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::fmt::Debug;
5use std::path::Path;
6use std::{fs, process};
7
8use crate::commands::config::network;
9use crate::config::locator::{print_deprecation_warning, Location};
10use crate::config::{alias, locator};
11
12#[derive(Parser, Debug, Clone)]
13#[group(skip)]
14pub struct Cmd {
15 #[command(flatten)]
16 pub config_locator: locator::Args,
17}
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error(transparent)]
22 Locator(#[from] locator::Error),
23
24 #[error(transparent)]
25 Network(#[from] network::Error),
26
27 #[error(transparent)]
28 IoError(#[from] std::io::Error),
29}
30
31#[derive(Debug, Clone)]
32struct AliasEntry {
33 alias: String,
34 contract: String,
35}
36
37impl Cmd {
38 pub fn run(&self) -> Result<(), Error> {
39 let config_dirs = self.config_locator.local_and_global()?;
40
41 for cfg in config_dirs {
42 match cfg {
43 Location::Local(config_dir) => {
44 if config_dir.exists() {
45 print_deprecation_warning(&config_dir);
46 }
47 }
48 Location::Global(config_dir) => Self::read_from_config_dir(&config_dir)?,
49 }
50 }
51
52 Ok(())
53 }
54
55 fn collect_aliases(config_dir: &Path) -> Result<HashMap<String, Vec<AliasEntry>>, Error> {
56 let contract_ids_dir = config_dir.join("contract-ids");
57 let mut map: HashMap<String, Vec<AliasEntry>> = HashMap::new();
58
59 if !contract_ids_dir.is_dir() {
60 return Ok(map);
61 }
62
63 for entry in fs::read_dir(&contract_ids_dir)? {
64 let path = entry?.path();
65
66 if path.extension() != Some(OsStr::new("json")) {
67 continue;
68 }
69
70 if let Some(alias) = path.file_stem() {
71 let alias = alias.to_string_lossy().into_owned();
72 let content = fs::read_to_string(&path)?;
73 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
74
75 for (network_passphrase, contract_id) in &data.ids {
76 let entry = AliasEntry {
77 alias: alias.clone(),
78 contract: contract_id.clone(),
79 };
80
81 map.entry(network_passphrase.clone())
82 .or_default()
83 .push(entry);
84 }
85 }
86 }
87
88 Ok(map)
89 }
90
91 fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
92 let mut map = Self::collect_aliases(config_dir)?;
93 let mut found = false;
94
95 for (network_passphrase, list) in &mut map {
96 println!("ℹ️ Aliases available for network '{network_passphrase}'");
97
98 list.sort_by(|a, b| a.alias.cmp(&b.alias));
99
100 for entry in list.iter() {
101 found = true;
102 println!("{}: {}", entry.alias, entry.contract);
103 }
104
105 println!();
106 }
107
108 if !found {
109 eprintln!("⚠️ No aliases defined for network");
110
111 process::exit(1);
112 }
113
114 Ok(())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::fs;
122
123 fn write_alias(dir: &Path, name: &str, network: &str, contract: &str) {
124 let contract_ids_dir = dir.join("contract-ids");
125 fs::create_dir_all(&contract_ids_dir).unwrap();
126 let content = format!(r#"{{"ids":{{"{network}":"{contract}"}}}}"#);
127 fs::write(contract_ids_dir.join(format!("{name}.json")), content).unwrap();
128 }
129
130 #[test]
131 fn glob_metacharacters_in_config_dir_are_treated_as_literal() {
132 let tmp = tempfile::tempdir().unwrap();
133 let base = tmp.path();
134
135 write_alias(&base.join("cfg1"), "alpha", "testnet", "CAAAA");
137 write_alias(&base.join("cfg2"), "beta", "testnet", "CBBBB");
138
139 write_alias(&base.join("cfg[12]"), "gamma", "testnet", "CCCCC");
141
142 let map = Cmd::collect_aliases(&base.join("cfg[12]")).unwrap();
143
144 let aliases: Vec<&str> = map
145 .values()
146 .flat_map(|entries| entries.iter().map(|e| e.alias.as_str()))
147 .collect();
148
149 assert!(
150 aliases.contains(&"gamma"),
151 "should read alias from the literal directory"
152 );
153 assert!(
154 !aliases.contains(&"alpha"),
155 "should not read from sibling cfg1"
156 );
157 assert!(
158 !aliases.contains(&"beta"),
159 "should not read from sibling cfg2"
160 );
161 }
162}