use crate::CrateName;
use crate::RustdocData;
use crate::search::SearchIndex;
use crate::sources::{CrateProvenance, DocsRsSource, LocalSource, Source, StdSource};
use elsa::sync::FrozenMap;
use fieldwork::Fieldwork;
use semver::Version;
use semver::VersionReq;
use std::borrow::Cow;
use std::fmt;
use std::fmt::Debug;
use std::path::PathBuf;
pub(crate) fn parse_docsrs_url(url: &str) -> Option<(&str, &str)> {
let url = url
.strip_prefix("https://docs.rs/")
.or_else(|| url.strip_prefix("http://docs.rs/"))?;
let parts: Vec<&str> = url.split('/').collect();
if parts.len() >= 2 {
Some((parts[0], parts[1]))
} else {
None
}
}
#[derive(Debug, Clone)]
struct ExternalCrateInfo {
name: String,
version: Version,
}
#[derive(Debug, Clone, Fieldwork)]
#[fieldwork(get, rename_predicates)]
pub struct CrateInfo {
#[field(copy)]
pub(crate) provenance: CrateProvenance,
pub(crate) version: Option<Version>,
pub(crate) description: Option<String>,
pub(crate) name: String,
pub(crate) default_crate: bool,
pub(crate) used_by: Vec<String>,
pub(crate) json_path: Option<PathBuf>,
}
#[derive(Fieldwork, Default)]
#[fieldwork(get, opt_in, with)]
pub struct Navigator {
#[field]
std_source: Option<StdSource>,
#[field]
docsrs_source: Option<DocsRsSource>,
#[field]
local_source: Option<LocalSource>,
pub(crate) working_set: FrozenMap<CrateName<'static>, Box<Option<RustdocData>>>,
external_crate_names: FrozenMap<CrateName<'static>, Box<ExternalCrateInfo>>,
pub(crate) search_indexes: FrozenMap<CrateName<'static>, Box<Option<SearchIndex>>>,
}
impl Debug for Navigator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Navigator")
.field("std_source", &self.std_source)
.field("docsrs_source", &self.docsrs_source)
.field("local_source", &self.local_source)
.finish()
}
}
impl Navigator {
pub fn list_available_crates(&self) -> impl Iterator<Item = &CrateInfo> {
std::iter::empty()
.chain(self.std_source.iter().flat_map(|x| x.list_available()))
.chain(self.local_source.iter().flat_map(|x| x.list_available()))
}
pub fn lookup_crate<'a>(
&'a self,
name: &str,
version: &VersionReq,
) -> Option<Cow<'a, CrateInfo>> {
log::info!("Resolving {name:?}, version {version}");
self.std_source()
.and_then(|s| s.lookup(name, version))
.or_else(|| self.local_source().and_then(|s| s.lookup(name, version)))
.or_else(|| self.docsrs_source().and_then(|s| s.lookup(name, version)))
}
pub fn project_root(&self) -> Option<&std::path::Path> {
self.local_source.as_ref().map(|p| p.project_root())
}
pub fn canonicalize(&self, name: &str) -> CrateName<'static> {
self.std_source()
.and_then(|s| s.canonicalize(name))
.or_else(|| self.local_source().and_then(|s| s.canonicalize(name)))
.or_else(|| self.docsrs_source().and_then(|s| s.canonicalize(name)))
.unwrap_or_else(|| CrateName::from(String::from(name)))
}
pub fn load_crate(&self, name: &str, version_req: &VersionReq) -> Option<&RustdocData> {
let crate_name = self.canonicalize(name);
if let Some(data) = self.working_set.get(&crate_name) {
return data.as_ref();
}
log::info!("Loading {name}@{version_req}");
let (resolved_name, resolved_version, provenance_hint) =
if let Some(external_crate) = self.external_crate_names.get(&crate_name) {
log::debug!("Found {crate_name} in external_crates");
(
external_crate.name.to_string(),
Some(external_crate.version.clone()),
None,
)
} else {
let lookup_result = self.lookup_crate(name, version_req)?;
(
lookup_result.name.to_string(),
lookup_result.version.clone(),
Some(lookup_result.provenance),
)
};
if let Some(rv) = resolved_version.as_ref() {
log::info!("Resolved {resolved_name}@{rv}");
} else {
log::info!("Resolved {resolved_name}");
}
let start = std::time::Instant::now();
let result = self.load(&resolved_name, resolved_version.as_ref(), provenance_hint);
let elapsed = start.elapsed();
log::debug!("⏱️ Total load time for {}: {:?}", resolved_name, elapsed);
match result {
Some(mut data) => {
self.index_external_crates(&data);
data.build_path_index();
self.working_set
.insert(CrateName::from(resolved_name), Box::new(Some(data)))
.as_ref()
}
None => {
self.working_set
.insert(CrateName::from(resolved_name), Box::new(None));
None
}
}
}
fn load(
&self,
crate_name: &str,
version: Option<&Version>,
provenance_hint: Option<CrateProvenance>,
) -> Option<RustdocData> {
match provenance_hint {
Some(CrateProvenance::Std) => {
log::debug!("loading from std");
self.std_source()?.load(crate_name, version)
}
Some(CrateProvenance::Workspace | CrateProvenance::LocalDependency) => {
log::debug!("loading from local");
self.local_source()?.load(crate_name, version)
}
Some(CrateProvenance::DocsRs) => {
log::debug!("loading from docs.rs");
self.docsrs_source()?.load(crate_name, version)
}
None => {
log::debug!("No provenance hint available, cascading lookup for {crate_name}");
self.std_source()
.and_then(|s| s.load(crate_name, version))
.or_else(|| {
self.local_source()
.and_then(|s| s.load(crate_name, version))
})
.or_else(|| {
self.docsrs_source()
.and_then(|s| s.load(crate_name, version))
})
}
}
}
fn index_external_crates(&self, crate_data: &RustdocData) {
log::debug!("Indexing external crates from {}", crate_data.name());
for external in crate_data.external_crates.values() {
if let Some(url) = &external.html_root_url
&& let Some((real_name, version)) = parse_docsrs_url(url)
&& let Ok(version) = Version::parse(version)
{
log::trace!("{}@{}", real_name, version);
let info = ExternalCrateInfo {
name: real_name.to_string(),
version,
};
self.external_crate_names
.insert(CrateName::from(external.name.clone()), Box::new(info));
}
}
}
}
#[allow(dead_code)]
const _: () = {
const fn assert_send<T: Send>() {}
const fn assert_sync<T: Sync>() {}
const fn check_navigator_thread_safety() {
assert_send::<Navigator>();
assert_sync::<Navigator>();
}
};