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#[derive(Debug, Clone)]
14pub struct CrateCache {
15 pub(crate) storage: CacheStorage,
16 downloader: CrateDownloader,
17 doc_generator: DocGenerator,
18}
19
20impl CrateCache {
21 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 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 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 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 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 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 tracing::info!("Generating docs for {}-{}", name, version);
111 match self.generate_docs(name, version).await {
112 Ok(_) => {
113 self.load_docs(name, version, None).await
115 }
116 Err(e) if e.to_string().contains("This is a binary-only package") => {
117 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 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 if self.storage.has_docs(name, version, Some(member_path)) {
139 return self.load_docs(name, version, Some(member_path)).await;
140 }
141
142 if !self.storage.is_cached(name, version) {
144 self.download_or_copy_crate(name, version, source).await?;
145 }
146
147 self.generate_workspace_member_docs(name, version, member_path)
149 .await?;
150
151 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 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 self.storage.save_metadata_with_source(
168 name,
169 version,
170 source.unwrap_or("unknown"),
171 None,
172 Some(member_info),
173 )?;
174
175 self.load_docs(name, version, Some(member_path)).await
177 }
178
179 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 let Some(member_path) = member {
188 return self
189 .ensure_workspace_member_docs(name, version, None, member_path)
190 .await;
191 }
192
193 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 if cargo_toml_path.exists() && WorkspaceHandler::is_workspace(&cargo_toml_path)? {
200 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 self.ensure_crate_docs(name, version, None).await
214 }
215
216 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 pub async fn generate_docs(&self, name: &str, version: &str) -> Result<PathBuf> {
230 self.doc_generator.generate_docs(name, version).await
231 }
232
233 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 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 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 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 pub async fn remove_crate(&self, name: &str, version: &str) -> Result<()> {
287 self.storage.remove_crate(name, version)
288 }
289
290 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 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 pub fn get_source_path(&self, name: &str, version: &str) -> Result<PathBuf> {
318 self.storage.source_path(name, version)
319 }
320
321 pub async fn ensure_crate_source(
323 &self,
324 name: &str,
325 version: &str,
326 source: Option<&str>,
327 ) -> Result<PathBuf> {
328 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 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 let source_path = self.ensure_crate_source(name, version, source).await?;
346
347 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 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 Ok(source_path)
380 }
381
382 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 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 let Some(members) = members {
398 let response = self
399 .cache_workspace_members(crate_name, version, members, source_str, true)
400 .await;
401
402 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 let source_path = self
416 .download_or_copy_crate(crate_name, version, source_str)
417 .await?;
418
419 let cargo_toml_path = source_path.join("Cargo.toml");
421 if WorkspaceHandler::is_workspace(&cargo_toml_path)? {
422 let members = WorkspaceHandler::get_workspace_members(&cargo_toml_path)?;
424 Ok(self.generate_workspace_response(crate_name, version, members, source, true))
425 } else {
426 self.ensure_crate_docs(crate_name, version, source_str)
428 .await?;
429
430 Ok(CacheResponse::success_updated(crate_name, version))
431 }
432 }
433
434 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) = ¶ms.branch {
449 branch.clone()
450 } else if let Some(tag) = ¶ms.tag {
451 tag.clone()
452 } else {
453 String::new()
455 };
456
457 let source_str = if let Some(branch) = ¶ms.branch {
458 Some(format!("{}#branch:{branch}", params.github_url))
459 } else if let Some(tag) = ¶ms.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 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 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 let results_with_members = join_all(member_futures).await;
518
519 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 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 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 let mut transaction = CacheTransaction::new(&self.storage, crate_name, version);
577
578 if let Err(e) = transaction.begin() {
580 return CacheResponse::error(format!("Failed to start update transaction: {e}"))
581 .to_json();
582 }
583
584 let update_result = self
586 .cache_crate_with_update_impl(crate_name, version, members, source_str, source)
587 .await;
588
589 match update_result {
591 Ok(response) => {
592 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 CacheResponse::error(format!("Update failed, restored from backup: {e}")).to_json()
604 }
605 }
606 }
607
608 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 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 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 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 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 let cargo_content = match std::fs::read_to_string(&cargo_toml_path) {
674 Ok(content) => content,
675 Err(_) => {
676 return self
678 .cache_regular_crate(crate_name, version, source_str)
679 .await;
680 }
681 };
682
683 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 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 self.cache_regular_crate(crate_name, version, source_str)
700 .await
701 }
702 }
703 }
704 }
705
706 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 async fn resolve_local_path_version(
720 &self,
721 params: &crate::cache::tools::CacheCrateFromLocalParams,
722 ) -> Result<(String, bool)> {
723 let expanded_path = shellexpand::full(¶ms.path)
725 .with_context(|| format!("Failed to expand path: {}", params.path))?;
726 let local_path = Path::new(expanded_path.as_ref());
727
728 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 if WorkspaceHandler::is_workspace(&cargo_toml)? {
740 match ¶ms.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 let actual_version = WorkspaceHandler::get_package_version(&cargo_toml)?;
752
753 match ¶ms.version {
754 Some(provided_version) => {
755 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)) }
765 None => {
766 Ok((actual_version, true)) }
769 }
770 }
771 }
772
773 pub async fn cache_crate_with_source(&self, source: CrateSource) -> String {
775 let source = if let CrateSource::LocalPath(mut params) = source {
777 match self.resolve_local_path_version(¶ms).await {
778 Ok((resolved_version, auto_detected)) => {
779 params.version = Some(resolved_version.clone());
781
782 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 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 if matches!(&source, CrateSource::GitHub(_)) && version.is_empty() {
816 return CacheResponse::error("Either branch or tag must be specified").to_json();
817 }
818
819 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 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 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 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 }
875
876 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 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 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 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 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}