oracle_lib/utils/
crate_check.rs1use std::path::{Path, PathBuf};
4use std::process::Command;
5
6#[derive(Debug, Clone)]
8pub struct CrateAvailability {
9 pub name: String,
10 pub is_installed: bool,
11 pub installed_version: Option<String>,
12 pub latest_version: Option<String>,
13 pub is_local: bool,
14 pub local_path: Option<PathBuf>,
15}
16
17impl CrateAvailability {
18 pub fn needs_install(&self) -> bool {
20 !self.is_installed && !self.is_local
21 }
22
23 pub fn has_update(&self) -> bool {
25 if let (Some(installed), Some(latest)) = (&self.installed_version, &self.latest_version) {
26 installed != latest && version_compare(installed, latest).is_lt()
27 } else {
28 false
29 }
30 }
31
32 pub fn install_command(&self) -> String {
34 if self.is_local {
35 if let Some(path) = &self.local_path {
36 return format!("cargo add --path {}", path.display());
37 }
38 }
39 format!("cargo add {}", self.name)
40 }
41}
42
43fn version_compare(a: &str, b: &str) -> std::cmp::Ordering {
45 let parse_version = |v: &str| -> Vec<u32> {
46 v.trim_start_matches('v')
47 .split('.')
48 .filter_map(|s| s.split('-').next()?.parse().ok())
49 .collect()
50 };
51
52 let va = parse_version(a);
53 let vb = parse_version(b);
54
55 va.cmp(&vb)
56}
57
58pub fn check_crate_in_registry(name: &str) -> Option<String> {
60 let cargo_home = dirs::home_dir()?.join(".cargo");
62 let registry_src = cargo_home.join("registry").join("src");
63
64 if !registry_src.exists() {
65 return None;
66 }
67
68 for entry in std::fs::read_dir(®istry_src).ok()? {
70 let registry_path = entry.ok()?.path();
71 for crate_entry in std::fs::read_dir(registry_path).ok()? {
72 let crate_path = crate_entry.ok()?.path();
73 let dir_name = crate_path.file_name()?.to_string_lossy();
74
75 if let Some(crate_name) = dir_name.rsplit('-').next_back() {
77 if crate_name == name {
78 let version = dir_name
80 .strip_prefix(name)
81 .and_then(|s| s.strip_prefix('-'))
82 .map(String::from);
83 return version;
84 }
85 }
86 }
87 }
88
89 None
90}
91
92pub fn check_crate_binary(name: &str) -> bool {
94 let cargo_bin = dirs::home_dir()
95 .map(|h| h.join(".cargo").join("bin"))
96 .unwrap_or_default();
97
98 let binary_name = if cfg!(windows) {
99 format!("{}.exe", name)
100 } else {
101 name.to_string()
102 };
103
104 cargo_bin.join(&binary_name).exists()
105}
106
107pub fn get_locked_version(project_path: &Path, crate_name: &str) -> Option<String> {
109 let lock_path = project_path.join("Cargo.lock");
110 if !lock_path.exists() {
111 return None;
112 }
113
114 let content = std::fs::read_to_string(&lock_path).ok()?;
115
116 let mut in_package = false;
118 let mut current_name = String::new();
119
120 for line in content.lines() {
121 let line = line.trim();
122
123 if line == "[[package]]" {
124 in_package = true;
125 current_name.clear();
126 continue;
127 }
128
129 if in_package {
130 if line.starts_with("name = ") {
131 current_name = line.strip_prefix("name = ")?.trim_matches('"').to_string();
132 } else if line.starts_with("version = ") && current_name == crate_name {
133 return Some(
134 line.strip_prefix("version = ")?
135 .trim_matches('"')
136 .to_string(),
137 );
138 } else if line.is_empty() || line.starts_with('[') {
139 in_package = false;
140 }
141 }
142 }
143
144 None
145}
146
147pub fn fetch_latest_version_sync(crate_name: &str) -> Option<String> {
149 let output = Command::new("cargo")
151 .args(["search", crate_name, "--limit", "1"])
152 .output()
153 .ok()?;
154
155 if !output.status.success() {
156 return None;
157 }
158
159 let stdout = String::from_utf8_lossy(&output.stdout);
160 for line in stdout.lines() {
161 if line.starts_with(crate_name) {
162 let parts: Vec<&str> = line.split('"').collect();
164 if parts.len() >= 2 {
165 return Some(parts[1].to_string());
166 }
167 }
168 }
169
170 None
171}
172
173pub fn check_availability(name: &str, project_path: Option<&PathBuf>) -> CrateAvailability {
175 let is_local = project_path.is_some();
176 let local_path = project_path.cloned();
177
178 let installed_version = project_path.and_then(|p| get_locked_version(p, name));
180
181 let registry_version = check_crate_in_registry(name);
183
184 CrateAvailability {
185 name: name.to_string(),
186 is_installed: installed_version.is_some() || registry_version.is_some(),
187 installed_version: installed_version.or(registry_version),
188 latest_version: None, is_local,
190 local_path,
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct CrateSuggestion {
197 pub action: SuggestedAction,
198 pub command: String,
199 pub description: String,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum SuggestedAction {
204 Install,
205 Update,
206 AddDependency,
207 ViewDocs,
208 ViewSource,
209}
210
211impl CrateSuggestion {
212 pub fn install(name: &str) -> Self {
213 Self {
214 action: SuggestedAction::Install,
215 command: format!("cargo add {}", name),
216 description: format!("Add {} to your project dependencies", name),
217 }
218 }
219
220 pub fn update(name: &str, version: &str) -> Self {
221 Self {
222 action: SuggestedAction::Update,
223 command: format!("cargo update -p {}@{}", name, version),
224 description: format!("Update {} to version {}", name, version),
225 }
226 }
227
228 pub fn view_docs(name: &str) -> Self {
229 Self {
230 action: SuggestedAction::ViewDocs,
231 command: format!("cargo doc -p {} --open", name),
232 description: format!("Open documentation for {}", name),
233 }
234 }
235
236 pub fn view_online_docs(name: &str) -> Self {
237 Self {
238 action: SuggestedAction::ViewDocs,
239 command: format!("xdg-open https://docs.rs/{}", name),
240 description: format!("Open docs.rs page for {}", name),
241 }
242 }
243}
244
245pub fn generate_suggestions(availability: &CrateAvailability) -> Vec<CrateSuggestion> {
247 let mut suggestions = Vec::new();
248
249 if availability.needs_install() {
250 suggestions.push(CrateSuggestion::install(&availability.name));
251 }
252
253 if availability.has_update() {
254 if let Some(ref version) = availability.latest_version {
255 suggestions.push(CrateSuggestion::update(&availability.name, version));
256 }
257 }
258
259 suggestions.push(CrateSuggestion::view_online_docs(&availability.name));
260
261 if availability.is_installed {
262 suggestions.push(CrateSuggestion::view_docs(&availability.name));
263 }
264
265 suggestions
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_version_compare() {
274 assert!(version_compare("1.0.0", "1.0.1").is_lt());
275 assert!(version_compare("1.0.1", "1.0.0").is_gt());
276 assert!(version_compare("1.0.0", "1.0.0").is_eq());
277 assert!(version_compare("0.9.0", "1.0.0").is_lt());
278 }
279}