use std::collections;
use std::fmt;
use std::fs;
use std::io;
use std::path;
use crate::doc;
#[derive(Debug)]
pub struct Index {
path: path::PathBuf,
data: Data,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct IndexItem {
pub name: doc::Fqn,
pub description: String,
}
impl fmt::Display for IndexItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.description.is_empty() {
write!(f, "{}", &self.name)
} else {
write!(f, "{}: {}", &self.name, &self.description)
}
}
}
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
#[serde(transparent)]
struct Data {
crates: collections::HashMap<String, CrateData>,
}
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
struct CrateData {
#[serde(rename = "i")]
items: Vec<ItemData>,
#[serde(rename = "p")]
paths: Vec<(usize, String)>,
}
#[derive(Debug, PartialEq, serde_tuple::Deserialize_tuple)]
struct ItemData {
ty: doc::ItemType,
name: String,
path: String,
desc: String,
parent: Option<usize>,
_ignored: serde_json::Value,
}
impl Index {
pub fn load(path: impl AsRef<path::Path>) -> anyhow::Result<Option<Self>> {
use std::io::BufRead;
anyhow::ensure!(
path.as_ref().is_file(),
"Search index '{}' must be a file",
path.as_ref().display()
);
let mut json: Option<String> = None;
let mut finished = false;
for line in io::BufReader::new(fs::File::open(path.as_ref())?).lines() {
let line = line?;
if let Some(json) = &mut json {
if line == "}');" {
json.push_str("}");
finished = true;
break;
} else {
json.push_str(line.trim_end_matches('\\'));
}
} else if line == "var searchIndex = JSON.parse('{\\" {
json = Some(String::from("{"));
}
}
if let Some(json) = json {
if finished {
use anyhow::Context;
let json = json.replace("\\'", "'");
let data: Data =
serde_json::from_str(&json).context("Could not parse search index")?;
Ok(Some(Index {
data,
path: path.as_ref().to_owned(),
}))
} else {
log::info!(
"Did not find JSON end line in search index '{}'",
path.as_ref().display()
);
Ok(None)
}
} else {
log::info!(
"Did not find JSON start line in search index '{}'",
path.as_ref().display()
);
Ok(None)
}
}
pub fn find(&self, name: &doc::Name) -> Vec<IndexItem> {
log::info!(
"Looking up '{}' in search index '{}'",
name,
self.path.display()
);
let mut matches: Vec<IndexItem> = Vec::new();
for (krate, data) in &self.data.crates {
let mut path = krate;
for item in &data.items {
path = if item.path.is_empty() {
path
} else {
&item.path
};
if item.ty == doc::ItemType::AssocType {
continue;
}
let full_path = match item.parent {
Some(idx) => {
let parent = &data.paths[idx].1;
format!("{}::{}", path, parent)
}
None => path.to_owned(),
};
let full_name: doc::Fqn = format!("{}::{}", &full_path, &item.name).into();
if full_name.ends_with(&name) {
log::info!("Found index match '{}'", full_name);
matches.push(IndexItem {
name: full_name,
description: item.desc.clone(),
});
}
}
}
matches.sort_unstable();
matches.dedup();
matches
}
}
#[cfg(test)]
mod tests {
use super::{CrateData, Data, ItemData};
use crate::doc::ItemType;
#[test]
fn test_empty() {
let expected: Data = Default::default();
let actual: Data = serde_json::from_str("{}").unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_empty_crate() {
let mut expected: Data = Default::default();
expected
.crates
.insert("test".to_owned(), Default::default());
let actual: Data = serde_json::from_str("{\"test\": {\"i\": [], \"p\": []}}").unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_one_item() {
let mut expected: Data = Default::default();
let mut krate: CrateData = Default::default();
krate.items.push(ItemData {
ty: ItemType::Module,
name: "name".to_owned(),
path: "path".to_owned(),
desc: "desc".to_owned(),
parent: None,
_ignored: Default::default(),
});
expected.crates.insert("test".to_owned(), krate);
let actual: Data = serde_json::from_str(
"{\"test\": {\"i\": [[0, \"name\", \"path\", \"desc\", null, null]], \"p\": []}}",
)
.unwrap();
assert_eq!(expected, actual);
}
}