rust_docs_mcp/cache/
service.rs

1use crate::cache::constants::*;
2use crate::cache::docgen::DocGenerator;
3use crate::cache::downloader::{CrateDownloader, CrateSource};
4use crate::cache::member_utils::normalize_member_path;
5use crate::cache::storage::{CacheStorage, MemberInfo};
6use crate::cache::transaction::CacheTransaction;
7use crate::cache::utils::CacheResponse;
8use crate::cache::workspace::WorkspaceHandler;
9use anyhow::{Context, Result, bail};
10use std::path::{Path, PathBuf};
11
12/// Service for managing crate caching and documentation generation
13#[derive(Debug, Clone)]
14pub struct CrateCache {
15    pub(crate) storage: CacheStorage,
16    downloader: CrateDownloader,
17    doc_generator: DocGenerator,
18}
19
20impl CrateCache {
21    /// Create a new crate cache instance
22    pub fn new(cache_dir: Option<PathBuf>) -> Result<Self> {
23        let storage = CacheStorage::new(cache_dir)?;
24        let downloader = CrateDownloader::new(storage.clone());
25        let doc_generator = DocGenerator::new(storage.clone());
26
27        Ok(Self {
28            storage,
29            downloader,
30            doc_generator,
31        })
32    }
33
34    /// Ensure a crate's documentation is available, downloading and generating if necessary
35    pub async fn ensure_crate_docs(
36        &self,
37        name: &str,
38        version: &str,
39        source: Option<&str>,
40    ) -> Result<rustdoc_types::Crate> {
41        tracing::info!("ensure_crate_docs called for {}-{}", name, version);
42
43        // Check if docs already exist
44        if self.storage.has_docs(name, version, None) {
45            tracing::info!(
46                "Docs already exist for {}-{}, loading from cache",
47                name,
48                version
49            );
50            return self.load_docs(name, version, None).await;
51        }
52
53        // Check if crate is downloaded but docs not generated
54        if !self.storage.is_cached(name, version) {
55            tracing::info!("Crate {}-{} not cached, downloading", name, version);
56            self.download_or_copy_crate(name, version, source).await?;
57        } else {
58            tracing::info!(
59                "Crate {}-{} already cached, skipping download",
60                name,
61                version
62            );
63        }
64
65        // Before generating docs, check if this is a workspace
66        let source_path = self.storage.source_path(name, version)?;
67        let cargo_toml_path = source_path.join("Cargo.toml");
68
69        tracing::info!(
70            "ensure_crate_docs: checking workspace for {} at {}",
71            name,
72            cargo_toml_path.display()
73        );
74
75        if cargo_toml_path.exists() {
76            tracing::info!("ensure_crate_docs: Cargo.toml exists for {}", name);
77            match WorkspaceHandler::is_workspace(&cargo_toml_path) {
78                Ok(true) => {
79                    tracing::info!("ensure_crate_docs: {} is a workspace", name);
80                    // It's a workspace without member specified
81                    let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)?;
82                    bail!(
83                        "This is a workspace crate. Please specify a member using the 'member' parameter.\n\
84                        Available members: {:?}\n\
85                        Example: specify member=\"{}\"",
86                        members,
87                        members.first().unwrap_or(&"crates/example".to_string())
88                    );
89                }
90                Ok(false) => {
91                    tracing::info!("ensure_crate_docs: {} is NOT a workspace", name);
92                }
93                Err(e) => {
94                    tracing::warn!(
95                        "ensure_crate_docs: error checking workspace status for {}: {}",
96                        name,
97                        e
98                    );
99                }
100            }
101        } else {
102            tracing::warn!(
103                "ensure_crate_docs: Cargo.toml does not exist for {} at {}",
104                name,
105                cargo_toml_path.display()
106            );
107        }
108
109        // Generate documentation
110        tracing::info!("Generating docs for {}-{}", name, version);
111        match self.generate_docs(name, version).await {
112            Ok(_) => {
113                // Load and return the generated docs
114                self.load_docs(name, version, None).await
115            }
116            Err(e) if e.to_string().contains("This is a binary-only package") => {
117                // This is a binary-only package, return appropriate error
118                bail!(
119                    "Cannot generate documentation for binary-only package '{}'. \
120                    This package contains only binary targets and no library to document. \
121                    rustdoc can only generate documentation for library targets.",
122                    name
123                )
124            }
125            Err(e) => Err(e),
126        }
127    }
128
129    /// Ensure a workspace member's documentation is available
130    pub async fn ensure_workspace_member_docs(
131        &self,
132        name: &str,
133        version: &str,
134        source: Option<&str>,
135        member_path: &str,
136    ) -> Result<rustdoc_types::Crate> {
137        // Check if docs already exist for this member
138        if self.storage.has_docs(name, version, Some(member_path)) {
139            return self.load_docs(name, version, Some(member_path)).await;
140        }
141
142        // Check if crate is downloaded
143        if !self.storage.is_cached(name, version) {
144            self.download_or_copy_crate(name, version, source).await?;
145        }
146
147        // Generate documentation for the specific workspace member
148        self.generate_workspace_member_docs(name, version, member_path)
149            .await?;
150
151        // Get package name for the member
152        let member_cargo_toml = self
153            .storage
154            .source_path(name, version)?
155            .join(member_path)
156            .join(CARGO_TOML);
157        let package_name = WorkspaceHandler::get_package_name(&member_cargo_toml)?;
158
159        // Create member info
160        let member_info = MemberInfo {
161            original_path: member_path.to_string(),
162            normalized_path: normalize_member_path(member_path),
163            package_name,
164        };
165
166        // Save unified metadata
167        self.storage.save_metadata_with_source(
168            name,
169            version,
170            source.unwrap_or("unknown"),
171            None,
172            Some(member_info),
173        )?;
174
175        // Load and return the generated docs
176        self.load_docs(name, version, Some(member_path)).await
177    }
178
179    /// Ensure documentation is available for a crate or workspace member
180    pub async fn ensure_crate_or_member_docs(
181        &self,
182        name: &str,
183        version: &str,
184        member: Option<&str>,
185    ) -> Result<rustdoc_types::Crate> {
186        // If member is specified, use workspace member logic
187        if let Some(member_path) = member {
188            return self
189                .ensure_workspace_member_docs(name, version, None, member_path)
190                .await;
191        }
192
193        // Check if crate is already downloaded
194        if self.storage.is_cached(name, version) {
195            let source_path = self.storage.source_path(name, version)?;
196            let cargo_toml_path = source_path.join("Cargo.toml");
197
198            // Check if it's a workspace
199            if cargo_toml_path.exists() && WorkspaceHandler::is_workspace(&cargo_toml_path)? {
200                // It's a workspace without member specified
201                let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)?;
202                bail!(
203                    "This is a workspace crate. Please specify a member using the 'member' parameter.\n\
204                    Available members: {:?}\n\
205                    Example: specify member=\"{}\"",
206                    members,
207                    members.first().unwrap_or(&"crates/example".to_string())
208                );
209            }
210        }
211
212        // Regular crate, use normal flow
213        self.ensure_crate_docs(name, version, None).await
214    }
215
216    /// Download or copy a crate based on source type
217    pub async fn download_or_copy_crate(
218        &self,
219        name: &str,
220        version: &str,
221        source: Option<&str>,
222    ) -> Result<PathBuf> {
223        self.downloader
224            .download_or_copy_crate(name, version, source)
225            .await
226    }
227
228    /// Generate JSON documentation for a crate
229    pub async fn generate_docs(&self, name: &str, version: &str) -> Result<PathBuf> {
230        self.doc_generator.generate_docs(name, version).await
231    }
232
233    /// Generate JSON documentation for a workspace member
234    pub async fn generate_workspace_member_docs(
235        &self,
236        name: &str,
237        version: &str,
238        member_path: &str,
239    ) -> Result<PathBuf> {
240        self.doc_generator
241            .generate_workspace_member_docs(name, version, member_path)
242            .await
243    }
244
245    /// Load documentation from cache for a crate or workspace member
246    pub async fn load_docs(
247        &self,
248        name: &str,
249        version: &str,
250        member_name: Option<&str>,
251    ) -> Result<rustdoc_types::Crate> {
252        let json_value = self
253            .doc_generator
254            .load_docs(name, version, member_name)
255            .await?;
256        let context_msg = if member_name.is_some() {
257            "Failed to parse member documentation JSON"
258        } else {
259            "Failed to parse documentation JSON"
260        };
261        let crate_docs: rustdoc_types::Crate =
262            serde_json::from_value(json_value).context(context_msg)?;
263        Ok(crate_docs)
264    }
265
266    /// Get cached versions of a crate
267    pub async fn get_cached_versions(&self, name: &str) -> Result<Vec<String>> {
268        let cached = self.storage.list_cached_crates()?;
269        let versions: Vec<String> = cached
270            .into_iter()
271            .filter(|meta| meta.name == name)
272            .map(|meta| meta.version)
273            .collect();
274
275        Ok(versions)
276    }
277
278    /// Get all cached crates with their metadata
279    pub async fn list_all_cached_crates(
280        &self,
281    ) -> Result<Vec<crate::cache::storage::CacheMetadata>> {
282        self.storage.list_cached_crates()
283    }
284
285    /// Remove a cached crate version
286    pub async fn remove_crate(&self, name: &str, version: &str) -> Result<()> {
287        self.storage.remove_crate(name, version)
288    }
289
290    /// Check if docs exist without ensuring they're generated
291    pub fn has_docs(&self, crate_name: &str, version: &str, member: Option<&str>) -> bool {
292        self.storage.has_docs(crate_name, version, member)
293    }
294
295    /// Try to load existing docs without generating
296    pub async fn try_load_docs(
297        &self,
298        crate_name: &str,
299        version: &str,
300        member: Option<&str>,
301    ) -> Result<Option<rustdoc_types::Crate>> {
302        if self.storage.has_docs(crate_name, version, member) {
303            if let Some(member_name) = member {
304                Ok(Some(
305                    self.load_docs(crate_name, version, Some(member_name))
306                        .await?,
307                ))
308            } else {
309                Ok(Some(self.load_docs(crate_name, version, None).await?))
310            }
311        } else {
312            Ok(None)
313        }
314    }
315
316    /// Get the source path for a crate
317    pub fn get_source_path(&self, name: &str, version: &str) -> Result<PathBuf> {
318        self.storage.source_path(name, version)
319    }
320
321    /// Ensure a crate's source is available, downloading if necessary (without generating docs)
322    pub async fn ensure_crate_source(
323        &self,
324        name: &str,
325        version: &str,
326        source: Option<&str>,
327    ) -> Result<PathBuf> {
328        // Check if crate is already downloaded
329        if !self.storage.is_cached(name, version) {
330            self.download_or_copy_crate(name, version, source).await?;
331        }
332
333        self.storage.source_path(name, version)
334    }
335
336    /// Ensure source is available for a crate or workspace member
337    pub async fn ensure_crate_or_member_source(
338        &self,
339        name: &str,
340        version: &str,
341        member: Option<&str>,
342        source: Option<&str>,
343    ) -> Result<PathBuf> {
344        // Ensure the crate source is downloaded
345        let source_path = self.ensure_crate_source(name, version, source).await?;
346
347        // If member is specified, return the member's source path
348        if let Some(member_path) = member {
349            let member_source_path = source_path.join(member_path);
350            let member_cargo_toml = member_source_path.join("Cargo.toml");
351
352            if !member_cargo_toml.exists() {
353                bail!(
354                    "Workspace member '{}' not found in {}-{}. \
355                    Make sure the member path is correct.",
356                    member_path,
357                    name,
358                    version
359                );
360            }
361
362            return Ok(member_source_path);
363        }
364
365        // Check if it's a workspace without member specified
366        let cargo_toml_path = source_path.join("Cargo.toml");
367        if cargo_toml_path.exists() && WorkspaceHandler::is_workspace(&cargo_toml_path)? {
368            let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)?;
369            bail!(
370                "This is a workspace crate. Please specify a member using the 'member' parameter.\n\
371                Available members: {:?}\n\
372                Example: specify member=\"{}\"",
373                members,
374                members.first().unwrap_or(&"crates/example".to_string())
375            );
376        }
377
378        // Regular crate, return source path
379        Ok(source_path)
380    }
381
382    /// Load dependency information from cache
383    pub async fn load_dependencies(&self, name: &str, version: &str) -> Result<serde_json::Value> {
384        self.doc_generator.load_dependencies(name, version).await
385    }
386
387    /// Internal implementation for caching a crate during update
388    async fn cache_crate_with_update_impl(
389        &self,
390        crate_name: &str,
391        version: &str,
392        members: &Option<Vec<String>>,
393        source_str: Option<&str>,
394        source: &CrateSource,
395    ) -> Result<CacheResponse> {
396        // If members are specified, cache those specific workspace members
397        if let Some(members) = members {
398            let response = self
399                .cache_workspace_members(crate_name, version, members, source_str, true)
400                .await;
401
402            // Check if all failed for proper error handling
403            if let CacheResponse::PartialSuccess {
404                results, errors, ..
405            } = &response
406                && results.is_empty()
407            {
408                bail!("Failed to update any workspace members: {:?}", errors);
409            }
410
411            return Ok(response);
412        }
413
414        // Download the crate
415        let source_path = self
416            .download_or_copy_crate(crate_name, version, source_str)
417            .await?;
418
419        // Check if it's a workspace
420        let cargo_toml_path = source_path.join("Cargo.toml");
421        if WorkspaceHandler::is_workspace(&cargo_toml_path)? {
422            // It's a workspace, get the members
423            let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)?;
424            Ok(self.generate_workspace_response(crate_name, version, members, source, true))
425        } else {
426            // Not a workspace, proceed with normal caching
427            self.ensure_crate_docs(crate_name, version, source_str)
428                .await?;
429
430            Ok(CacheResponse::success_updated(crate_name, version))
431        }
432    }
433
434    /// Extract source parameters from CrateSource enum
435    fn extract_source_params(
436        &self,
437        source: &CrateSource,
438    ) -> (String, String, Option<Vec<String>>, Option<String>, bool) {
439        match source {
440            CrateSource::CratesIO(params) => (
441                params.crate_name.clone(),
442                params.version.clone(),
443                params.members.clone(),
444                None,
445                params.update.unwrap_or(false),
446            ),
447            CrateSource::GitHub(params) => {
448                let version = if let Some(branch) = &params.branch {
449                    branch.clone()
450                } else if let Some(tag) = &params.tag {
451                    tag.clone()
452                } else {
453                    // This should not happen due to validation in the tool layer
454                    String::new()
455                };
456
457                let source_str = if let Some(branch) = &params.branch {
458                    Some(format!("{}#branch:{branch}", params.github_url))
459                } else if let Some(tag) = &params.tag {
460                    Some(format!("{}#tag:{tag}", params.github_url))
461                } else {
462                    Some(params.github_url.clone())
463                };
464
465                (
466                    params.crate_name.clone(),
467                    version,
468                    params.members.clone(),
469                    source_str,
470                    params.update.unwrap_or(false),
471                )
472            }
473            CrateSource::LocalPath(params) => (
474                params.crate_name.clone(),
475                params
476                    .version
477                    .clone()
478                    .expect("Version should be resolved before extraction"),
479                params.members.clone(),
480                Some(params.path.clone()),
481                params.update.unwrap_or(false),
482            ),
483        }
484    }
485
486    /// Handle caching workspace members
487    async fn cache_workspace_members(
488        &self,
489        crate_name: &str,
490        version: &str,
491        members: &[String],
492        source_str: Option<&str>,
493        updated: bool,
494    ) -> CacheResponse {
495        use futures::future::join_all;
496
497        // Create futures for all member caching operations
498        let member_futures: Vec<_> = members
499            .iter()
500            .map(|member| {
501                let member_clone = member.clone();
502                async move {
503                    let result = self
504                        .ensure_workspace_member_docs(
505                            crate_name,
506                            version,
507                            source_str,
508                            &member_clone,
509                        )
510                        .await;
511                    (member_clone, result)
512                }
513            })
514            .collect();
515
516        // Execute all futures concurrently
517        let results_with_members = join_all(member_futures).await;
518
519        // Collect results and errors
520        let mut results = Vec::new();
521        let mut errors = Vec::new();
522
523        for (member, result) in results_with_members {
524            match result {
525                Ok(_) => {
526                    results.push(format!("Successfully cached member: {member}"));
527                }
528                Err(e) => {
529                    errors.push(format!("Failed to cache member {member}: {e}"));
530                }
531            }
532        }
533
534        if errors.is_empty() {
535            CacheResponse::members_success(crate_name, version, members.to_vec(), results, updated)
536        } else {
537            CacheResponse::members_partial(
538                crate_name,
539                version,
540                members.to_vec(),
541                results,
542                errors,
543                updated,
544            )
545        }
546    }
547
548    /// Generate workspace detection response
549    fn generate_workspace_response(
550        &self,
551        crate_name: &str,
552        version: &str,
553        members: Vec<String>,
554        source: &CrateSource,
555        updated: bool,
556    ) -> CacheResponse {
557        let source_type = match source {
558            CrateSource::CratesIO(_) => "cratesio",
559            CrateSource::GitHub(_) => "github",
560            CrateSource::LocalPath(_) => "local",
561        };
562
563        CacheResponse::workspace_detected(crate_name, version, members, source_type, updated)
564    }
565
566    /// Handle update operation for a crate
567    async fn handle_crate_update(
568        &self,
569        crate_name: &str,
570        version: &str,
571        members: &Option<Vec<String>>,
572        source_str: Option<&str>,
573        source: &CrateSource,
574    ) -> String {
575        // Create transaction for safe update
576        let mut transaction = CacheTransaction::new(&self.storage, crate_name, version);
577
578        // Begin transaction (creates backup and removes existing cache)
579        if let Err(e) = transaction.begin() {
580            return CacheResponse::error(format!("Failed to start update transaction: {e}"))
581                .to_json();
582        }
583
584        // Try to re-cache the crate
585        let update_result = self
586            .cache_crate_with_update_impl(crate_name, version, members, source_str, source)
587            .await;
588
589        // Check if update was successful
590        match update_result {
591            Ok(response) => {
592                // Success - commit transaction
593                if let Err(e) = transaction.commit() {
594                    return CacheResponse::error(format!(
595                        "Update succeeded but failed to cleanup: {e}"
596                    ))
597                    .to_json();
598                }
599                response.to_json()
600            }
601            Err(e) => {
602                // Failed - transaction will automatically rollback on drop
603                CacheResponse::error(format!("Update failed, restored from backup: {e}")).to_json()
604            }
605        }
606    }
607
608    /// Handle workspace members caching
609    async fn handle_workspace_members(
610        &self,
611        crate_name: &str,
612        version: &str,
613        members: &[String],
614        source_str: Option<&str>,
615        updated: bool,
616    ) -> CacheResponse {
617        self.cache_workspace_members(crate_name, version, members, source_str, updated)
618            .await
619    }
620
621    /// Detect and handle workspace crates
622    async fn detect_and_handle_workspace(
623        &self,
624        crate_name: &str,
625        version: &str,
626        source_path: &std::path::Path,
627        source: &CrateSource,
628        source_str: Option<&str>,
629        updated: bool,
630    ) -> Result<CacheResponse> {
631        let cargo_toml_path = source_path.join("Cargo.toml");
632
633        tracing::info!(
634            "detect_and_handle_workspace: checking {}",
635            cargo_toml_path.display()
636        );
637
638        // Read and log the content to debug workspace detection
639        if let Ok(content) = std::fs::read_to_string(&cargo_toml_path) {
640            tracing::info!(
641                "detect_and_handle_workspace: Cargo.toml content preview for {}: {}",
642                crate_name,
643                &content[0..content.len().min(500)]
644            );
645        }
646
647        match WorkspaceHandler::is_workspace(&cargo_toml_path) {
648            Ok(true) => {
649                tracing::info!("detect_and_handle_workspace: {} is a workspace", crate_name);
650                // It's a workspace, get the members
651                let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)
652                    .context("Failed to get workspace members")?;
653                Ok(self.generate_workspace_response(crate_name, version, members, source, updated))
654            }
655            Ok(false) => {
656                tracing::info!(
657                    "detect_and_handle_workspace: {} is NOT a workspace",
658                    crate_name
659                );
660                // Not a workspace, proceed with normal caching
661                self.cache_regular_crate(crate_name, version, source_str)
662                    .await
663            }
664            Err(e) => {
665                tracing::warn!(
666                    "detect_and_handle_workspace: error checking workspace status for {}: {}",
667                    crate_name,
668                    e
669                );
670                // Error checking workspace status - try to determine if it's likely a workspace
671                // by checking for common workspace indicators to avoid attempting doc generation
672                // on workspace roots
673                let cargo_content = match std::fs::read_to_string(&cargo_toml_path) {
674                    Ok(content) => content,
675                    Err(_) => {
676                        // Can't read Cargo.toml, fall back to normal caching
677                        return self
678                            .cache_regular_crate(crate_name, version, source_str)
679                            .await;
680                    }
681                };
682
683                // Check for workspace indicators even if parsing failed
684                if cargo_content.contains("[workspace]") && cargo_content.contains("members") {
685                    tracing::warn!(
686                        "detect_and_handle_workspace: {} appears to be a workspace based on content analysis, \
687                        but parsing failed. Treating as workspace to avoid doc generation errors",
688                        crate_name
689                    );
690
691                    // Return a generic workspace response indicating we couldn't parse the members
692                    let error_msg = format!(
693                        "Detected workspace but failed to parse members: {e}. \
694                        Please check the Cargo.toml syntax or cache specific members manually."
695                    );
696                    Ok(CacheResponse::error(error_msg))
697                } else {
698                    // Doesn't look like a workspace, try normal caching
699                    self.cache_regular_crate(crate_name, version, source_str)
700                        .await
701                }
702            }
703        }
704    }
705
706    /// Cache a regular (non-workspace) crate
707    async fn cache_regular_crate(
708        &self,
709        crate_name: &str,
710        version: &str,
711        source_str: Option<&str>,
712    ) -> Result<CacheResponse> {
713        self.ensure_crate_docs(crate_name, version, source_str)
714            .await?;
715        Ok(CacheResponse::success(crate_name, version))
716    }
717
718    /// Resolve version for local paths
719    async fn resolve_local_path_version(
720        &self,
721        params: &crate::cache::tools::CacheCrateFromLocalParams,
722    ) -> Result<(String, bool)> {
723        // Expand path to handle ~ and other shell expansions
724        let expanded_path = shellexpand::full(&params.path)
725            .with_context(|| format!("Failed to expand path: {}", params.path))?;
726        let local_path = Path::new(expanded_path.as_ref());
727
728        // Check if path exists
729        if !local_path.exists() {
730            bail!("Local path does not exist: {}", local_path.display());
731        }
732
733        let cargo_toml = local_path.join("Cargo.toml");
734        if !cargo_toml.exists() {
735            bail!("No Cargo.toml found at path: {}", local_path.display());
736        }
737
738        // Check if this is a workspace manifest
739        if WorkspaceHandler::is_workspace(&cargo_toml)? {
740            // For workspaces, we don't have a version in the manifest
741            // The version must be provided by the user
742            match &params.version {
743                Some(provided_version) => Ok((provided_version.clone(), false)),
744                None => bail!(
745                    "The path '{}' contains a workspace manifest. Please provide a version for caching.",
746                    local_path.display()
747                ),
748            }
749        } else {
750            // Get the actual version from Cargo.toml
751            let actual_version = WorkspaceHandler::get_package_version(&cargo_toml)?;
752
753            match &params.version {
754                Some(provided_version) => {
755                    // Version was provided, validate it matches
756                    if provided_version != &actual_version {
757                        bail!(
758                            "Version mismatch: provided version '{}' does not match actual version '{}' in Cargo.toml",
759                            provided_version,
760                            actual_version
761                        );
762                    }
763                    Ok((actual_version, false)) // Version was validated, not auto-detected
764                }
765                None => {
766                    // No version provided, use the detected one
767                    Ok((actual_version, true)) // Version was auto-detected
768                }
769            }
770        }
771    }
772
773    /// Common method to cache a crate from any source
774    pub async fn cache_crate_with_source(&self, source: CrateSource) -> String {
775        // For local paths, resolve version if needed
776        let source = if let CrateSource::LocalPath(mut params) = source {
777            match self.resolve_local_path_version(&params).await {
778                Ok((resolved_version, auto_detected)) => {
779                    // Update params with resolved version
780                    params.version = Some(resolved_version.clone());
781
782                    // Log if version was auto-detected
783                    if auto_detected {
784                        tracing::info!(
785                            "Auto-detected version '{}' from local path for crate '{}'",
786                            resolved_version,
787                            params.crate_name
788                        );
789                    }
790
791                    CrateSource::LocalPath(params)
792                }
793                Err(e) => {
794                    return CacheResponse::error(format!("Failed to resolve local path: {e}"))
795                        .to_json();
796                }
797            }
798        } else {
799            source
800        };
801
802        // Extract parameters from source
803        let (crate_name, version, members, source_str, update) =
804            self.extract_source_params(&source);
805
806        tracing::info!(
807            "cache_crate_with_source: starting for {}-{}, update={}, members={:?}",
808            crate_name,
809            version,
810            update,
811            members
812        );
813
814        // Validate GitHub source
815        if matches!(&source, CrateSource::GitHub(_)) && version.is_empty() {
816            return CacheResponse::error("Either branch or tag must be specified").to_json();
817        }
818
819        // Handle update logic if requested
820        if update && self.storage.is_cached(&crate_name, &version) {
821            tracing::info!(
822                "cache_crate_with_source: {} is cached and update requested",
823                crate_name
824            );
825            return self
826                .handle_crate_update(
827                    &crate_name,
828                    &version,
829                    &members,
830                    source_str.as_deref(),
831                    &source,
832                )
833                .await;
834        }
835
836        // If members are specified, cache those specific workspace members
837        if let Some(members) = members {
838            tracing::info!(
839                "cache_crate_with_source: members specified for {}: {:?}",
840                crate_name,
841                members
842            );
843            let response = self
844                .handle_workspace_members(
845                    &crate_name,
846                    &version,
847                    &members,
848                    source_str.as_deref(),
849                    false,
850                )
851                .await;
852            return response.to_json();
853        }
854
855        // Check if already cached (unless update was requested)
856        if !update && self.storage.is_cached(&crate_name, &version) {
857            tracing::info!(
858                "cache_crate_with_source: {} is already cached, checking docs",
859                crate_name
860            );
861            // Check if docs are generated
862            if self.storage.has_docs(&crate_name, &version, None) {
863                tracing::info!(
864                    "cache_crate_with_source: {} docs exist, returning success",
865                    crate_name
866                );
867                return CacheResponse::success(&crate_name, &version).to_json();
868            }
869            tracing::info!(
870                "cache_crate_with_source: {} is cached but docs not generated, continuing",
871                crate_name
872            );
873            // Crate is cached but docs not generated, continue to generate docs
874        }
875
876        // First, download the crate if not already cached
877        let source_path = match self
878            .download_or_copy_crate(&crate_name, &version, source_str.as_deref())
879            .await
880        {
881            Ok(path) => {
882                tracing::info!(
883                    "cache_crate_with_source: source downloaded/available at {}",
884                    path.display()
885                );
886                path
887            }
888            Err(e) => {
889                return CacheResponse::error(format!("Failed to download crate: {e}")).to_json();
890            }
891        };
892
893        tracing::info!(
894            "cache_crate_with_source: calling detect_and_handle_workspace for {}",
895            crate_name
896        );
897        // Detect and handle workspace vs regular crate
898        match self
899            .detect_and_handle_workspace(
900                &crate_name,
901                &version,
902                &source_path,
903                &source,
904                source_str.as_deref(),
905                false,
906            )
907            .await
908        {
909            Ok(response) => {
910                tracing::info!(
911                    "cache_crate_with_source: detect_and_handle_workspace succeeded for {}",
912                    crate_name
913                );
914                response.to_json()
915            }
916            Err(e) => {
917                tracing::error!(
918                    "cache_crate_with_source: detect_and_handle_workspace failed for {}: {}",
919                    crate_name,
920                    e
921                );
922                // Check if this is the workspace error we're looking for
923                if e.to_string()
924                    .contains("This appears to be a workspace with multiple targets")
925                {
926                    tracing::error!(
927                        "cache_crate_with_source: ERROR - workspace detection failed, error came from rustdoc generation"
928                    );
929                }
930
931                // Extract more specific error context based on the source type
932                let error_msg = match &source {
933                    CrateSource::CratesIO(_) => {
934                        format!(
935                            "Failed to cache crate '{crate_name}' version '{version}' from crates.io: {e}"
936                        )
937                    }
938                    CrateSource::GitHub(params) => {
939                        let ref_info = params
940                            .branch
941                            .as_ref()
942                            .map(|b| format!("branch '{b}'"))
943                            .or_else(|| params.tag.as_ref().map(|t| format!("tag '{t}'")))
944                            .unwrap_or_else(|| "default branch".to_string());
945
946                        format!(
947                            "Failed to cache crate '{}' from GitHub repository '{}' ({}): {}",
948                            crate_name, params.github_url, ref_info, e
949                        )
950                    }
951                    CrateSource::LocalPath(params) => {
952                        format!(
953                            "Failed to cache crate '{}' from local path '{}': {}",
954                            crate_name, params.path, e
955                        )
956                    }
957                };
958                CacheResponse::error(error_msg).to_json()
959            }
960        }
961    }
962
963    /// Create search index for a crate or workspace member (exposed for search module)
964    pub async fn create_search_index(
965        &self,
966        name: &str,
967        version: &str,
968        member_name: Option<&str>,
969    ) -> Result<()> {
970        self.doc_generator
971            .create_search_index(name, version, member_name)
972            .await
973    }
974}