Skip to content

Add search alias for Rust official crates #2868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[toolchain]
channel = "1.88.0"
components = ["rustfmt", "clippy"]
4 changes: 3 additions & 1 deletion src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ impl AxumRouterTestExt for axum::Router {
// }

if redirect_target != expected_target {
anyhow::bail!("got redirect to {redirect_target}");
anyhow::bail!(
"got redirect to `{redirect_target}`, expected redirect to `{expected_target}`",
);
}

Ok(response)
Expand Down
108 changes: 93 additions & 15 deletions src/web/releases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct Release {
pub(crate) build_time: Option<DateTime<Utc>>,
pub(crate) stars: i32,
pub(crate) has_unyanked_releases: Option<bool>,
pub(crate) href: Option<&'static str>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -124,6 +125,7 @@ pub(crate) async fn get_releases(
build_time: row.get(5),
stars: row.get::<Option<i32>, _>(6).unwrap_or(0),
has_unyanked_releases: None,
href: None,
})
.try_collect()
.await?)
Expand All @@ -142,13 +144,28 @@ struct SearchResult {
pub next_page: Option<String>,
}

fn rust_lib_release(name: &str, description: &str, href: &'static str) -> ReleaseStatus {
ReleaseStatus::Available(Release {
name: name.to_string(),
version: String::new(),
description: Some(description.to_string()),
build_time: None,
target_name: None,
rustdoc_status: false,
stars: 0,
has_unyanked_releases: None,
href: Some(href),
})
}

/// Get the search results for a crate search query
///
/// This delegates to the crates.io search API.
async fn get_search_results(
conn: &mut sqlx::PgConnection,
registry: &RegistryApi,
query_params: &str,
query: &str,
) -> Result<SearchResult, anyhow::Error> {
let crate::registry_api::Search { crates, meta } = registry.search(query_params).await?;

Expand Down Expand Up @@ -206,28 +223,38 @@ async fn get_search_results(
rustdoc_status: row.rustdoc_status.unwrap_or(false),
stars: row.stars.unwrap_or(0),
has_unyanked_releases: row.has_unyanked_releases,
href: None,
},
)
})
.try_collect()
.await?;

// start with the original names from crates.io to keep the original ranking,
// extend with the release/build information from docs.rs
// Crates that are not on docs.rs yet will not be returned.
let mut results = Vec::new();
if let Some(super::rustdoc::OfficialCrateDescription {
name,
href,
description,
}) = super::rustdoc::DOC_RUST_LANG_ORG_REDIRECTS.get(query)
{
results.push(rust_lib_release(name, description, href))
}

let names: Vec<String> =
Arc::into_inner(names).expect("Arc still borrowed in `get_search_results`");
results.extend(names.into_iter().map(|name| {
if let Some(release) = crates.remove(&name) {
ReleaseStatus::Available(release)
} else {
ReleaseStatus::NotAvailable(name)
}
}));

Ok(SearchResult {
// start with the original names from crates.io to keep the original ranking,
// extend with the release/build information from docs.rs
// Crates that are not on docs.rs yet will not be returned.
results: names
.into_iter()
.map(|name| {
if let Some(release) = crates.remove(&name) {
ReleaseStatus::Available(release)
} else {
ReleaseStatus::NotAvailable(name)
}
})
.collect(),
results,
prev_page: meta.prev_page,
next_page: meta.next_page,
})
Expand Down Expand Up @@ -589,15 +616,15 @@ pub(crate) async fn search_handler(
}
}

get_search_results(&mut conn, &registry, query_params).await?
get_search_results(&mut conn, &registry, query_params, "").await?
} else if !query.is_empty() {
let query_params: String = form_urlencoded::Serializer::new(String::new())
.append_pair("q", &query)
.append_pair("sort", &sort_by)
.append_pair("per_page", &RELEASES_IN_RELEASES.to_string())
.finish();

get_search_results(&mut conn, &registry, &query_params).await?
get_search_results(&mut conn, &registry, &query_params, &query).await?
} else {
return Err(AxumNope::NoResults);
};
Expand Down Expand Up @@ -2231,4 +2258,55 @@ mod tests {
Ok(())
});
}

#[test]
fn test_search_std() {
async_wrapper(|env| async move {
let web = env.web_app().await;

async fn inner(web: &axum::Router, krate: &str) -> Result<(), anyhow::Error> {
let full = kuchikiki::parse_html().one(
web.get(&format!("/releases/search?query={krate}"))
.await?
.text()
.await?,
);
let items = full
.select("ul a.release")
.expect("missing list items")
.collect::<Vec<_>>();

// empty because expand_rebuild_queue is not set
let item_element = items.first().unwrap();
let item = item_element.as_node();
assert_eq!(
item.select(".name")
.unwrap()
.next()
.unwrap()
.text_contents(),
"std"
);
assert_eq!(
item.select(".description")
.unwrap()
.next()
.unwrap()
.text_contents(),
"Rust standard library",
);
assert_eq!(
item_element.attributes.borrow().get("href").unwrap(),
"https://doc.rust-lang.org/stable/std/"
);

Ok(())
}

inner(&web, "std").await?;
inner(&web, "libstd").await?;

Ok(())
});
}
}
139 changes: 125 additions & 14 deletions src/web/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,129 @@ use tracing::{Instrument, debug, error, info_span, instrument, trace};

use super::extractors::PathFileExtension;

static DOC_RUST_LANG_ORG_REDIRECTS: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
HashMap::from([
("alloc", "stable/alloc"),
("core", "stable/core"),
("proc_macro", "stable/proc_macro"),
("proc-macro", "stable/proc_macro"),
("std", "stable/std"),
("test", "stable/test"),
("rustc", "nightly/nightly-rustc"),
("rustdoc", "nightly/nightly-rustc/rustdoc"),
])
});
pub(crate) struct OfficialCrateDescription {
pub(crate) name: &'static str,
pub(crate) href: &'static str,
pub(crate) description: &'static str,
}

pub(crate) static DOC_RUST_LANG_ORG_REDIRECTS: Lazy<HashMap<&str, OfficialCrateDescription>> =
Lazy::new(|| {
HashMap::from([
(
"alloc",
OfficialCrateDescription {
name: "alloc",
href: "https://doc.rust-lang.org/stable/alloc/",
description: "Rust alloc library",
},
),
(
"liballoc",
OfficialCrateDescription {
name: "alloc",
href: "https://doc.rust-lang.org/stable/alloc/",
description: "Rust alloc library",
},
),
(
"core",
OfficialCrateDescription {
name: "core",
href: "https://doc.rust-lang.org/stable/core/",
description: "Rust core library",
},
),
(
"libcore",
OfficialCrateDescription {
name: "core",
href: "https://doc.rust-lang.org/stable/core/",
description: "Rust core library",
},
),
(
"proc_macro",
OfficialCrateDescription {
name: "proc_macro",
href: "https://doc.rust-lang.org/stable/proc_macro/",
description: "Rust proc_macro library",
},
),
(
"libproc_macro",
OfficialCrateDescription {
name: "proc_macro",
href: "https://doc.rust-lang.org/stable/proc_macro/",
description: "Rust proc_macro library",
},
),
(
"proc-macro",
OfficialCrateDescription {
name: "proc_macro",
href: "https://doc.rust-lang.org/stable/proc_macro/",
description: "Rust proc_macro library",
},
),
(
"libproc-macro",
OfficialCrateDescription {
name: "proc_macro",
href: "https://doc.rust-lang.org/stable/proc_macro/",
description: "Rust proc_macro library",
},
),
(
"std",
OfficialCrateDescription {
name: "std",
href: "https://doc.rust-lang.org/stable/std/",
description: "Rust standard library",
},
),
(
"libstd",
OfficialCrateDescription {
name: "std",
href: "https://doc.rust-lang.org/stable/std/",
description: "Rust standard library",
},
),
(
"test",
OfficialCrateDescription {
name: "test",
href: "https://doc.rust-lang.org/stable/test/",
description: "Rust test library",
},
),
(
"libtest",
OfficialCrateDescription {
name: "test",
href: "https://doc.rust-lang.org/stable/test/",
description: "Rust test library",
},
),
(
"rustc",
OfficialCrateDescription {
name: "rustc",
href: "https://doc.rust-lang.org/nightly/nightly-rustc/",
description: "rustc API",
},
),
(
"rustdoc",
OfficialCrateDescription {
name: "rustdoc",
href: "https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/",
description: "rustdoc API",
},
),
])
});

#[derive(Debug, Clone, Deserialize)]
pub(crate) struct RustdocRedirectorParams {
Expand Down Expand Up @@ -151,10 +262,10 @@ pub(crate) async fn rustdoc_redirector_handler(
None => (params.name.to_string(), None),
};

if let Some(inner_path) = DOC_RUST_LANG_ORG_REDIRECTS.get(crate_name.as_str()) {
if let Some(description) = DOC_RUST_LANG_ORG_REDIRECTS.get(crate_name.as_str()) {
return Ok(redirect_to_doc(
&query_pairs,
format!("https://doc.rust-lang.org/{inner_path}/"),
description.href.to_string(),
CachePolicy::ForeverInCdnAndStaleInBrowser,
path_in_crate.as_deref(),
)?
Expand Down
Loading
Loading