Skip to main content

oracle_lib/utils/
crate_check.rs

1//! Crate installation detection and management
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6/// Result of checking if a crate is available
7#[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    /// Check if a crate needs installation
19    pub fn needs_install(&self) -> bool {
20        !self.is_installed && !self.is_local
21    }
22
23    /// Check if an update is available
24    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    /// Generate install command suggestion
33    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
43/// Compare semantic versions (simplified)
44fn 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
58/// Check if a crate is available in the local cargo cache
59pub fn check_crate_in_registry(name: &str) -> Option<String> {
60    // Check cargo's registry cache
61    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    // Look for the crate in any registry
69    for entry in std::fs::read_dir(&registry_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            // Parse crate directory name (format: name-version)
76            if let Some(crate_name) = dir_name.rsplit('-').next_back() {
77                if crate_name == name {
78                    // Extract version from directory name
79                    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
92/// Check if a crate binary is installed
93pub 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
107/// Get installed crate version from Cargo.lock if available
108pub 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    // Simple Cargo.lock parser - look for package entries
117    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
147/// Fetch latest version from crates.io (sync version)
148pub fn fetch_latest_version_sync(crate_name: &str) -> Option<String> {
149    // Use cargo search for simple lookup
150    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            // Format: "crate_name = \"version\" # description"
163            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
173/// Check overall crate availability
174pub 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    // Check if installed in project
179    let installed_version = project_path.and_then(|p| get_locked_version(p, name));
180
181    // Check if in cargo registry cache
182    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, // Filled async if needed
189        is_local,
190        local_path,
191    }
192}
193
194/// Suggestions for common crate operations
195#[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
245/// Generate suggestions for a crate
246pub 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}