rust_docs_mcp/cache/
utils.rs

1//! Utility functions for the cache module
2//!
3//! This module contains shared utilities used across the cache implementation,
4//! including file operations, error handling, and response formatting.
5
6use super::outputs::CacheCrateOutput;
7use anyhow::{Context, Result, bail};
8use std::fs;
9use std::path::Path;
10
11/// Recursively copy directory contents from source to destination
12///
13/// This function copies all files and subdirectories from the source path to the destination,
14/// excluding version control directories like .git, .svn, and .hg.
15pub fn copy_directory_contents(src: &Path, dest: &Path) -> Result<()> {
16    if !src.exists() {
17        bail!("Source directory does not exist: {}", src.display());
18    }
19
20    if !dest.exists() {
21        fs::create_dir_all(dest)
22            .with_context(|| format!("Failed to create directory: {}", dest.display()))?;
23    }
24
25    for entry in
26        fs::read_dir(src).with_context(|| format!("Failed to read directory: {}", src.display()))?
27    {
28        let entry = entry?;
29        let path = entry.path();
30        let name = entry.file_name();
31        let dest_path = dest.join(&name);
32
33        if path.is_dir() {
34            // Skip version control directories and target directory
35            if name == ".git" || name == ".svn" || name == ".hg" || name == "target" {
36                continue;
37            }
38            copy_directory_contents(&path, &dest_path)?;
39        } else {
40            fs::copy(&path, &dest_path).with_context(|| {
41                format!(
42                    "Failed to copy file from {} to {}",
43                    path.display(),
44                    dest_path.display()
45                )
46            })?;
47        }
48    }
49
50    Ok(())
51}
52
53/// Format bytes into human-readable string
54pub fn format_bytes(bytes: u64) -> String {
55    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
56    if bytes == 0 {
57        return "0 B".to_string();
58    }
59
60    let base = 1024_f64;
61    let exponent = (bytes as f64).ln() / base.ln();
62    let exponent = exponent.floor() as usize;
63
64    let unit = UNITS.get(exponent).unwrap_or(&"TB");
65    let size = bytes as f64 / base.powi(exponent as i32);
66
67    if size.fract() == 0.0 {
68        format!("{size:.0} {unit}")
69    } else {
70        format!("{size:.2} {unit}")
71    }
72}
73
74/// Response types for cache operations - now using the outputs module
75pub type CacheResponse = CacheCrateOutput;
76
77impl CacheResponse {
78    /// Create a success response
79    pub fn success(crate_name: impl Into<String>, version: impl Into<String>) -> Self {
80        let crate_name = crate_name.into();
81        let version = version.into();
82        Self::Success {
83            message: format!("Successfully cached {crate_name}-{version}"),
84            crate_name,
85            version,
86            members: None,
87            results: None,
88            updated: None,
89        }
90    }
91
92    /// Create a success response with update flag
93    pub fn success_updated(crate_name: impl Into<String>, version: impl Into<String>) -> Self {
94        let crate_name = crate_name.into();
95        let version = version.into();
96        Self::Success {
97            message: format!("Successfully updated {crate_name}-{version}"),
98            crate_name,
99            version,
100            members: None,
101            results: None,
102            updated: Some(true),
103        }
104    }
105
106    /// Create a workspace members success response
107    pub fn members_success(
108        crate_name: impl Into<String>,
109        version: impl Into<String>,
110        members: Vec<String>,
111        results: Vec<String>,
112        updated: bool,
113    ) -> Self {
114        let count = results.len();
115        let message = if updated {
116            format!("Successfully updated {count} workspace members")
117        } else {
118            format!("Successfully cached {count} workspace members")
119        };
120
121        Self::Success {
122            message,
123            crate_name: crate_name.into(),
124            version: version.into(),
125            members: Some(members),
126            results: Some(results),
127            updated: if updated { Some(true) } else { None },
128        }
129    }
130
131    /// Create a partial success response for workspace members
132    pub fn members_partial(
133        crate_name: impl Into<String>,
134        version: impl Into<String>,
135        members: Vec<String>,
136        results: Vec<String>,
137        errors: Vec<String>,
138        updated: bool,
139    ) -> Self {
140        let message = if updated {
141            format!(
142                "Updated {} members with {} errors",
143                results.len(),
144                errors.len()
145            )
146        } else {
147            format!(
148                "Cached {} members with {} errors",
149                results.len(),
150                errors.len()
151            )
152        };
153
154        Self::PartialSuccess {
155            message,
156            crate_name: crate_name.into(),
157            version: version.into(),
158            members,
159            results,
160            errors,
161            updated: if updated { Some(true) } else { None },
162        }
163    }
164
165    /// Create a workspace detected response
166    pub fn workspace_detected(
167        crate_name: impl Into<String>,
168        version: impl Into<String>,
169        members: Vec<String>,
170        source_type: &str,
171        updated: bool,
172    ) -> Self {
173        let crate_name = crate_name.into();
174        let version = version.into();
175        let example_members = members.get(0..2.min(members.len())).unwrap_or(&[]).to_vec();
176
177        Self::WorkspaceDetected {
178            message: "This is a workspace crate. Please specify which members to cache using the 'members' parameter.".to_string(),
179            crate_name: crate_name.clone(),
180            version: version.clone(),
181            workspace_members: members,
182            example_usage: format!(
183                "cache_crate_from_{source_type}(crate_name=\"{crate_name}\", version=\"{version}\", members={example_members:?})"
184            ),
185            updated: if updated { Some(true) } else { None },
186        }
187    }
188
189    /// Create an error response
190    pub fn error(message: impl Into<String>) -> Self {
191        Self::Error {
192            error: message.into(),
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use tempfile::TempDir;
201
202    #[test]
203    fn test_format_bytes() {
204        assert_eq!(format_bytes(0), "0 B");
205        assert_eq!(format_bytes(512), "512 B");
206        assert_eq!(format_bytes(1024), "1 KB");
207        assert_eq!(format_bytes(1536), "1.50 KB");
208        assert_eq!(format_bytes(1048576), "1 MB");
209        assert_eq!(format_bytes(1073741824), "1 GB");
210    }
211
212    #[test]
213    fn test_copy_directory_contents() -> Result<()> {
214        let temp_dir = TempDir::new()?;
215        let src_dir = temp_dir.path().join("src");
216        let dest_dir = temp_dir.path().join("dest");
217
218        // Create source structure
219        fs::create_dir_all(&src_dir)?;
220        fs::write(src_dir.join("file1.txt"), "content1")?;
221
222        let sub_dir = src_dir.join("subdir");
223        fs::create_dir_all(&sub_dir)?;
224        fs::write(sub_dir.join("file2.txt"), "content2")?;
225
226        // Create .git directory that should be skipped
227        let git_dir = src_dir.join(".git");
228        fs::create_dir_all(&git_dir)?;
229        fs::write(git_dir.join("config"), "git config")?;
230
231        // Copy contents
232        copy_directory_contents(&src_dir, &dest_dir)?;
233
234        // Verify
235        assert!(dest_dir.join("file1.txt").exists());
236        assert!(dest_dir.join("subdir").exists());
237        assert!(dest_dir.join("subdir/file2.txt").exists());
238        assert!(!dest_dir.join(".git").exists());
239
240        Ok(())
241    }
242
243    #[test]
244    fn test_cache_response() {
245        // Test success response
246        let response = CacheResponse::success("test-crate", "1.0.0");
247        let json_str = response.to_json();
248        let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
249        assert_eq!(json["status"], "success");
250        assert_eq!(json["message"], "Successfully cached test-crate-1.0.0");
251        assert_eq!(json["crate"], "test-crate");
252        assert_eq!(json["version"], "1.0.0");
253
254        // Test error response
255        let error = CacheResponse::error("Something went wrong");
256        let json_str = error.to_json();
257        let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
258        assert_eq!(json["status"], "error");
259        assert_eq!(json["error"], "Something went wrong");
260
261        // Test workspace detected
262        let workspace = CacheResponse::workspace_detected(
263            "test-crate",
264            "1.0.0",
265            vec!["crate-a".to_string(), "crate-b".to_string()],
266            "cratesio",
267            false,
268        );
269        let json_str = workspace.to_json();
270        let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
271        assert_eq!(json["status"], "workspace_detected");
272        assert_eq!(json["crate"], "test-crate");
273        assert_eq!(
274            json["workspace_members"],
275            serde_json::json!(["crate-a", "crate-b"])
276        );
277
278        // Test members success
279        let members = CacheResponse::members_success(
280            "test-crate",
281            "1.0.0",
282            vec!["member1".to_string()],
283            vec!["Successfully cached member: member1".to_string()],
284            false,
285        );
286        let json_str = members.to_json();
287        let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
288        assert_eq!(json["status"], "success");
289        assert!(json.get("updated").is_none());
290
291        // Test members success with update
292        let members_updated = CacheResponse::members_success(
293            "test-crate",
294            "1.0.0",
295            vec!["member1".to_string()],
296            vec!["Successfully cached member: member1".to_string()],
297            true,
298        );
299        let json_str = members_updated.to_json();
300        let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
301        assert!(json["updated"].as_bool().unwrap());
302    }
303}