use std::collections::{HashMap, HashSet};
use std::path::Path;
use rustdoc_types::{Crate, Id, ItemEnum, Visibility};
use serde::Serialize;
use super::{CrateCollection, RUST_PATH_SEP};
#[derive(Debug, Serialize)]
pub struct SearchEntry {
pub name: String,
pub path: String,
pub kind: &'static str,
#[serde(rename = "crate")]
pub crate_name: String,
pub file: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SearchIndex {
pub items: Vec<SearchEntry>,
}
pub struct SearchIndexGenerator<'a> {
crates: &'a CrateCollection,
include_private: bool,
rendered_items: HashMap<String, HashSet<Id>>,
}
impl<'a> SearchIndexGenerator<'a> {
#[must_use]
pub const fn new(
crates: &'a CrateCollection,
include_private: bool,
rendered_items: HashMap<String, HashSet<Id>>,
) -> Self {
Self {
crates,
include_private,
rendered_items,
}
}
#[must_use]
pub fn generate(&self) -> SearchIndex {
let mut items = Vec::new();
for (crate_name, krate) in self.crates.iter() {
self.index_crate(&mut items, crate_name, krate);
}
items.sort_by(|a, b| a.name.cmp(&b.name));
SearchIndex { items }
}
pub fn write(&self, output_dir: &Path) -> std::io::Result<()> {
let index = self.generate();
let json = serde_json::to_string_pretty(&index)?;
let path = output_dir.join("search_index.json");
fs_err::write(path, json)?;
Ok(())
}
fn index_crate(&self, items: &mut Vec<SearchEntry>, crate_name: &str, krate: &Crate) {
let rendered_set = self.rendered_items.get(crate_name);
let path_map = Self::build_path_map(krate);
for (id, item) in &krate.index {
let Some(name) = &item.name else { continue };
if let Some(rendered) = rendered_set
&& !rendered.contains(id)
{
continue;
}
if !self.include_private && !matches!(item.visibility, Visibility::Public) {
continue;
}
let kind = match &item.inner {
ItemEnum::Module(_) => "mod",
ItemEnum::Struct(_) => "struct",
ItemEnum::Enum(_) => "enum",
ItemEnum::Trait(_) => "trait",
ItemEnum::Function(_) => "fn",
ItemEnum::TypeAlias(_) => "type",
ItemEnum::Constant { .. } => "const",
ItemEnum::Macro(_) => "macro",
_ => continue,
};
let module_path = path_map.get(id).cloned().unwrap_or_default();
let full_path = if module_path.is_empty() {
format!("{crate_name}::{name}")
} else {
format!("{crate_name}::{module_path}::{name}")
};
let file = Self::compute_file_path(crate_name, &module_path, kind);
let summary = item
.docs
.as_ref()
.and_then(|d| d.lines().next())
.map(str::to_string);
items.push(SearchEntry {
name: name.clone(),
path: full_path,
kind,
crate_name: crate_name.to_string(),
file,
summary,
});
}
}
fn build_path_map(krate: &Crate) -> HashMap<Id, String> {
let mut path_map = HashMap::new();
for (id, item_summary) in &krate.paths {
if item_summary.crate_id != 0 {
continue;
}
let path_components = &item_summary.path;
if path_components.len() > 1 {
let module_path = path_components[1..path_components.len() - 1].join("::");
path_map.insert(*id, module_path);
} else {
path_map.insert(*id, String::new());
}
}
path_map
}
fn compute_file_path(crate_name: &str, module_path: &str, kind: &str) -> String {
if module_path.is_empty() {
format!("{crate_name}/index.md")
} else if kind == "mod" {
let path = module_path.replace(RUST_PATH_SEP, "/");
format!("{crate_name}/{path}/index.md")
} else {
let path = module_path.replace(RUST_PATH_SEP, "/");
format!("{crate_name}/{path}/index.md")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_entry_serialization() {
let entry = SearchEntry {
name: "Span".to_string(),
path: "tracing::span::Span".to_string(),
kind: "struct",
crate_name: "tracing".to_string(),
file: "tracing/span/index.md".to_string(),
summary: Some("A handle representing a span.".to_string()),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"name\":\"Span\""));
assert!(json.contains("\"kind\":\"struct\""));
assert!(json.contains("\"crate\":\"tracing\""));
}
#[test]
fn test_search_entry_without_summary() {
let entry = SearchEntry {
name: "foo".to_string(),
path: "crate::foo".to_string(),
kind: "fn",
crate_name: "crate".to_string(),
file: "crate/index.md".to_string(),
summary: None,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("summary"));
}
}