1use crate::Result;
4use asimov_env::paths::asimov_root;
5use asimov_module::{ModuleManifest, resolve::Resolver};
6use clientele::{Subcommand, SubcommandsProvider, SysexitsError::*};
7use miette::{IntoDiagnostic, miette};
8
9pub(crate) fn build_resolver(pattern: &str) -> miette::Result<Resolver> {
10 let mut resolver = Resolver::new();
11
12 let module_dir_path = asimov_root().join("modules");
13 let module_dir = std::fs::read_dir(&module_dir_path)
14 .map_err(|e| miette!("Failed to read module manifest directory: {e}"))?
15 .filter_map(Result::ok);
16
17 for entry in module_dir {
18 let filename = entry.file_name();
19 let filename = filename.to_string_lossy();
20 if !filename.ends_with(".yml") && !filename.ends_with(".yaml") {
21 continue;
22 }
23 let file = std::fs::File::open(entry.path()).into_diagnostic()?;
24
25 let manifest: ModuleManifest = serde_yml::from_reader(file).map_err(|e| {
26 miette!(
27 "Invalid module manifest at `{}`: {}",
28 entry.path().display(),
29 e
30 )
31 })?;
32
33 if !manifest
34 .provides
35 .programs
36 .iter()
37 .any(|program| program.split('-').next_back().is_some_and(|p| p == pattern))
38 {
39 continue;
40 }
41
42 resolver
43 .insert_manifest(&manifest)
44 .map_err(|e| miette!("{e}"))?;
45 }
46
47 Ok(resolver)
48}
49
50pub fn locate_subcommand(name: &str) -> Result<Subcommand> {
52 let libexec = asimov_root().join("libexec");
53 if libexec.exists() {
54 let file = std::fs::read_dir(libexec)?
55 .filter_map(Result::ok)
56 .find(|entry| {
57 entry.file_name().to_str().is_some_and(|filename| {
58 filename.starts_with("asimov-") && filename.ends_with(name)
59 })
60 });
61 if let Some(entry) = file {
62 return Ok(Subcommand {
63 name: format!("asimov-{}", name),
64 path: entry.path(),
65 });
66 }
67 }
68
69 match SubcommandsProvider::find("asimov-", name) {
70 Some(cmd) => Ok(cmd),
71 None => {
72 eprintln!("{}: command not found: {}{}", "asimov", "asimov-", name);
73 Err(EX_UNAVAILABLE)
74 },
75 }
76}
77
78pub fn normalize_url(url: &str) -> String {
79 if let Ok(url) = <url::Url>::parse(url) {
81 return url.to_string();
82 };
83
84 let url = url
88 .strip_prefix("~/")
89 .map(|path| {
90 std::env::home_dir()
91 .expect("unable to determine home directory")
92 .join(path)
93 })
94 .unwrap_or_else(|| std::path::PathBuf::from(url));
95
96 if let Ok(path) = std::path::Path::new(&url)
102 .canonicalize()
103 .map_err(|_| ())
104 .and_then(url::Url::from_file_path)
105 {
106 return path.to_string();
107 };
108
109 if let Ok(path) = std::path::absolute(&url)
112 .map_err(|_| ())
113 .and_then(url::Url::from_file_path)
114 {
115 return path.to_string();
116 }
117
118 if let Ok(path) = url::Url::from_file_path(std::path::Path::new(&url)) {
127 return path.to_string();
128 }
129
130 format!("file://{}", url.display())
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn url_normalization() {
141 let cases = [
142 ("https://example.org/", "https://example.org/"),
143 ("near://testnet/123456789", "near://testnet/123456789"),
144 ];
145
146 for case in cases {
147 assert_eq!(normalize_url(case.0), case.1, "input: {:?}", case.0);
148 }
149
150 #[cfg(unix)]
151 {
152 unsafe { std::env::set_var("HOME", "/home/user") };
153
154 let cases = [
155 ("~/path/to/file.txt", "file:///home/user/path/to/file.txt"),
156 ("/file with spaces.txt", "file:///file%20with%20spaces.txt"),
157 ("/file+with+pluses.txt", "file:///file+with+pluses.txt"),
158 ];
159
160 for case in cases {
161 assert_eq!(normalize_url(case.0), case.1, "input: {:?}", case.0);
162 }
163
164 let cur_dir = std::env::current_dir().unwrap().display().to_string();
165
166 let input = "path/to/file.txt";
167 let want = "file://".to_string() + &cur_dir + "/path/to/file.txt";
168 assert_eq!(
169 normalize_url(input),
170 want,
171 "relative path should be get added after current directory, input: {:?}",
172 input
173 );
174
175 let input = "../path/./file.txt";
176 let want = "file://".to_string() + &cur_dir + "/../path/file.txt";
177 assert_eq!(
178 normalize_url(input),
179 want,
180 "relative path should be get added after current directory, input: {:?}",
181 input
182 );
183
184 let input = "another-type-of-a-string";
185 let want = "file://".to_string() + &cur_dir + "/another-type-of-a-string";
186 assert_eq!(
187 normalize_url(input),
188 want,
189 "non-path-looking input should be treated as a file in current directory, input: {:?}",
190 input
191 );
192
193 let input = "hello\\ world!";
194 let want = "file://".to_string() + &cur_dir + "/hello%5C%20world!";
195 assert_eq!(
196 normalize_url(input),
197 want,
198 "output should be url encoded, input: {:?}",
199 input
200 );
201 }
202
203 #[cfg(windows)]
204 {
205 let cases = [
206 (
207 "/file with spaces.txt",
208 "file:///C:/file%20with%20spaces.txt",
209 ),
210 ("/file+with+pluses.txt", "file:///C:/file+with+pluses.txt"),
211 ];
212
213 for case in cases {
214 assert_eq!(normalize_url(case.0), case.1, "input: {:?}", case.0);
215 }
216 }
217 }
218}