use std::collections::BTreeMap;
use serde_json::{Map, Value};
#[derive(Clone, Copy)]
pub struct Docs<'a> {
primary: &'a Value,
aux: &'a [Value],
}
impl<'a> Docs<'a> {
pub fn single(primary: &'a Value) -> Self {
Self { primary, aux: &[] }
}
pub fn new(primary: &'a Value, aux: &'a [Value]) -> Self {
Self { primary, aux }
}
pub fn primary(self) -> &'a Value {
self.primary
}
pub fn aux(self) -> &'a [Value] {
self.aux
}
}
impl<'a> From<&'a Value> for Docs<'a> {
fn from(primary: &'a Value) -> Self {
Self::single(primary)
}
}
pub(super) struct Doc<'a> {
pub(super) index: &'a Map<String, Value>,
pub(super) paths: &'a Map<String, Value>,
pub(super) krate: String,
by_path: BTreeMap<String, String>,
by_name: BTreeMap<String, Option<String>>,
}
impl<'a> Doc<'a> {
pub(super) fn open(doc: &'a Value) -> Result<Self, String> {
let index = doc["index"]
.as_object()
.ok_or("rustdoc JSON has no `index` object")?;
let paths = doc["paths"]
.as_object()
.ok_or("rustdoc JSON has no `paths` object")?;
let mut d = Self {
index,
paths,
krate: crate_name(doc, paths),
by_path: BTreeMap::new(),
by_name: BTreeMap::new(),
};
for (id, entry) in paths {
if !matches!(
entry["kind"].as_str(),
Some("struct" | "enum" | "type_alias" | "typedef")
) || !d.index.contains_key(id)
{
continue;
}
let Some(path) = joined_path(entry) else {
continue;
};
let name = path.rsplit("::").next().unwrap_or(&path).to_string();
d.by_path.entry(path).or_insert_with(|| id.clone());
d.by_name
.entry(name)
.and_modify(|slot| *slot = None)
.or_insert_with(|| Some(id.clone()));
}
Ok(d)
}
pub(super) fn walkable(&self) -> impl Iterator<Item = &String> {
self.by_path.keys()
}
pub(super) fn item(&self, id: &Value) -> Option<&'a Value> {
self.index.get(&id_key(id)?)
}
pub(super) fn canonical(&self, id: &Value) -> Option<String> {
joined_path(self.paths.get(&id_key(id)?)?)
}
pub(super) fn serves_graphql(&self, item: &Value) -> bool {
let inner = &item["inner"];
let Some(impls) = inner
.get("struct")
.or_else(|| inner.get("enum"))
.and_then(|k| k["impls"].as_array())
else {
return false;
};
impls
.iter()
.filter_map(|id| self.item(id))
.filter_map(|im| self.canonical(&im["inner"]["impl"]["trait"]["id"]))
.any(|path| is_graphql_container_trait(&path))
}
pub(super) fn locate(&self, canonical: &str) -> Option<Value> {
if let Some(id) = self.by_path.get(canonical) {
return Some(Value::String(id.clone()));
}
let first = canonical.split("::").next()?;
if self.krate.is_empty() || first != self.krate {
return None;
}
self.locate_name(canonical.rsplit("::").next()?)
}
pub(super) fn locate_name(&self, name: &str) -> Option<Value> {
self.by_name
.get(name)?
.as_ref()
.map(|id| Value::String(id.clone()))
}
pub(super) fn scan_type_id(&self, name: &str) -> Option<Value> {
self.index
.iter()
.find(|(_, item)| {
item["name"].as_str() == Some(name)
&& (item["inner"].get("struct").is_some()
|| item["inner"].get("enum").is_some())
})
.and_then(|(k, _)| {
k.parse::<u64>()
.ok()
.map(Value::from)
.or_else(|| Some(Value::String(k.clone())))
})
}
}
fn id_key(id: &Value) -> Option<String> {
match id {
Value::Number(n) => Some(n.to_string()),
Value::String(s) => Some(s.clone()),
_ => None,
}
}
fn is_graphql_container_trait(path: &str) -> bool {
path.split("::").next() == Some("async_graphql")
&& matches!(
path.rsplit("::").next(),
Some(
"ContainerType"
| "ObjectType"
| "InterfaceType"
| "UnionType"
| "InputObjectType"
| "OneofObjectType"
| "EnumType"
)
)
}
fn joined_path(entry: &Value) -> Option<String> {
let segs: Vec<&str> = entry["path"]
.as_array()?
.iter()
.filter_map(|s| s.as_str())
.collect();
Some(segs.join("::"))
}
fn crate_name(doc: &Value, paths: &Map<String, Value>) -> String {
let root = doc
.get("root")
.and_then(id_key)
.and_then(|r| paths.get(&r))
.and_then(joined_path);
if let Some(name) = root.as_deref().and_then(|p| p.split("::").next())
&& !name.is_empty()
{
return name.to_string();
}
paths
.values()
.find(|e| e["crate_id"].as_u64() == Some(0))
.and_then(joined_path)
.and_then(|p| p.split("::").next().map(String::from))
.unwrap_or_default()
}