use std::fmt::Write;
use serde::Serialize;
use crate::types::{DisplayItem, GroupedItems, IndexItem, TraitImplInfo};
#[derive(Debug, Serialize)]
pub(crate) struct JsonDocItem {
pub(crate) path: String,
pub(crate) kind: String,
pub(crate) signature: String,
pub(crate) doc: String,
pub(crate) feature_gate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) reexported_from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) methods: Option<Vec<JsonMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) trait_impls: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) variants: Option<Vec<JsonVariant>>,
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonMethod {
pub(crate) name: String,
pub(crate) signature: String,
pub(crate) summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) has_body: Option<bool>,
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonVariant {
pub(crate) name: String,
pub(crate) signature: String,
pub(crate) summary: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonListItem {
pub(crate) path: String,
pub(crate) kind: String,
pub(crate) signature: String,
pub(crate) summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) reexported_from: Option<String>,
}
pub(crate) fn render_json(display: &DisplayItem<'_>) -> String {
match display {
DisplayItem::Crate { item, children } | DisplayItem::Module { item, children } => {
render_json_container(item, children)
}
DisplayItem::Type {
item,
methods,
variants,
trait_impls,
} => render_json_type(item, methods, variants, trait_impls),
DisplayItem::Trait {
item,
required_methods,
provided_methods,
} => render_json_trait(item, required_methods, provided_methods),
DisplayItem::Leaf { item } => render_json_leaf(item),
}
}
pub(crate) fn render_json_ambiguous(items: &[&IndexItem]) -> String {
let mut out = String::new();
for item in items {
let list_item = JsonListItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
summary: item.summary.clone(),
reexported_from: item.reexport_source.clone(),
};
let json = serde_json::to_string(&list_item).expect("invariant: JsonListItem serializes");
let _ = writeln!(out, "{json}");
}
trim_trailing_newlines(&mut out);
out
}
fn render_json_container(item: &IndexItem, children: &GroupedItems<'_>) -> String {
let mut out = String::new();
let doc_item = JsonDocItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
doc: item.docs.clone(),
feature_gate: item.feature_gate.clone(),
reexported_from: item.reexport_source.clone(),
methods: None,
trait_impls: None,
variants: None,
};
let json = serde_json::to_string(&doc_item).expect("invariant: JsonDocItem serializes");
let _ = writeln!(out, "{json}");
for group_items in children.values() {
for child in group_items {
let list_item = JsonListItem {
path: child.path.clone(),
kind: child.kind.short_name().to_string(),
signature: child.signature.clone(),
summary: child.summary.clone(),
reexported_from: child.reexport_source.clone(),
};
let json =
serde_json::to_string(&list_item).expect("invariant: JsonListItem serializes");
let _ = writeln!(out, "{json}");
}
}
trim_trailing_newlines(&mut out);
out
}
fn render_json_type(
item: &IndexItem,
methods: &[&IndexItem],
variants: &[&IndexItem],
trait_impls: &[TraitImplInfo],
) -> String {
let json_methods: Vec<JsonMethod> = methods
.iter()
.map(|m| JsonMethod {
name: m.name.clone(),
signature: m.signature.clone(),
summary: m.summary.clone(),
has_body: None,
})
.collect();
let json_variants: Vec<JsonVariant> = variants
.iter()
.map(|v| JsonVariant {
name: v.name.clone(),
signature: v.signature.clone(),
summary: v.summary.clone(),
})
.collect();
let json_trait_impls: Vec<String> =
trait_impls.iter().map(|ti| ti.trait_path.clone()).collect();
let doc_item = JsonDocItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
doc: item.docs.clone(),
feature_gate: item.feature_gate.clone(),
reexported_from: item.reexport_source.clone(),
methods: if json_methods.is_empty() {
None
} else {
Some(json_methods)
},
trait_impls: if json_trait_impls.is_empty() {
None
} else {
Some(json_trait_impls)
},
variants: if json_variants.is_empty() {
None
} else {
Some(json_variants)
},
};
serde_json::to_string(&doc_item).expect("invariant: JsonDocItem serializes")
}
fn render_json_trait(
item: &IndexItem,
required_methods: &[&IndexItem],
provided_methods: &[&IndexItem],
) -> String {
let mut json_methods: Vec<JsonMethod> = Vec::new();
for m in required_methods {
json_methods.push(JsonMethod {
name: m.name.clone(),
signature: m.signature.clone(),
summary: m.summary.clone(),
has_body: Some(false),
});
}
for m in provided_methods {
json_methods.push(JsonMethod {
name: m.name.clone(),
signature: m.signature.clone(),
summary: m.summary.clone(),
has_body: Some(true),
});
}
let doc_item = JsonDocItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
doc: item.docs.clone(),
feature_gate: item.feature_gate.clone(),
reexported_from: item.reexport_source.clone(),
methods: if json_methods.is_empty() {
None
} else {
Some(json_methods)
},
trait_impls: None,
variants: None,
};
serde_json::to_string(&doc_item).expect("invariant: JsonDocItem serializes")
}
fn render_json_leaf(item: &IndexItem) -> String {
let doc_item = JsonDocItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
doc: item.docs.clone(),
feature_gate: item.feature_gate.clone(),
reexported_from: item.reexport_source.clone(),
methods: None,
trait_impls: None,
variants: None,
};
serde_json::to_string(&doc_item).expect("invariant: JsonDocItem serializes")
}
pub(crate) fn render_json_recursive(items: &[&IndexItem]) -> String {
let mut out = String::new();
for item in items {
let list_item = JsonListItem {
path: item.path.clone(),
kind: item.kind.short_name().to_string(),
signature: item.signature.clone(),
summary: item.summary.clone(),
reexported_from: item.reexport_source.clone(),
};
let json = serde_json::to_string(&list_item).expect("invariant: JsonListItem serializes");
let _ = writeln!(out, "{json}");
}
trim_trailing_newlines(&mut out);
out
}
fn trim_trailing_newlines(s: &mut String) {
while s.ends_with('\n') {
s.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::build_display_item;
use crate::test_utils::make_item_full;
use crate::types::{ChildRef, DocIndex, ItemKind, TraitImplInfo};
#[test]
fn render_json_struct() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let m1 = make_item_full(
"lock",
"mycrate::Mutex::lock",
ItemKind::Function,
"pub fn lock(&self) -> MutexGuard<'_, T>",
"Locks this mutex.",
"Locks this mutex.",
);
let m2 = make_item_full(
"new",
"mycrate::Mutex::new",
ItemKind::Function,
"pub fn new(t: T) -> Self",
"Creates a new lock.",
"Creates a new lock.",
);
index.add_item(m1);
index.add_item(m2);
let mut struct_item = make_item_full(
"Mutex",
"mycrate::Mutex",
ItemKind::Struct,
"pub struct Mutex<T: ?Sized>",
"An asynchronous Mutex-like type.",
"An asynchronous Mutex-like type.",
);
struct_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Function,
name: "lock".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Function,
name: "new".to_string(),
},
];
index.add_item(struct_item);
index.trait_impls.insert(
2,
vec![
TraitImplInfo {
trait_path: "Debug".to_string(),
is_synthetic: false,
},
TraitImplInfo {
trait_path: "Send".to_string(),
is_synthetic: true,
},
],
);
let di = build_display_item(&index, 2, false, None);
let output = render_json(&di);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("should be valid JSON");
assert_eq!(parsed["path"], "mycrate::Mutex");
assert_eq!(parsed["kind"], "struct");
assert_eq!(parsed["signature"], "pub struct Mutex<T: ?Sized>");
assert!(parsed["methods"].is_array());
assert_eq!(parsed["methods"].as_array().unwrap().len(), 2);
assert!(parsed["trait_impls"].is_array());
assert_eq!(parsed["trait_impls"].as_array().unwrap().len(), 2);
assert!(parsed["variants"].is_null() || parsed.get("variants").is_none());
insta::assert_snapshot!(output);
}
#[test]
fn render_json_trait_view() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut req = make_item_full(
"next",
"mycrate::Iterator::next",
ItemKind::Function,
"fn next(&mut self) -> Option<Self::Item>",
"Advances the iterator.",
"Advances the iterator.",
);
req.has_body = false;
let mut prov = make_item_full(
"count",
"mycrate::Iterator::count",
ItemKind::Function,
"fn count(self) -> usize",
"Consumes the iterator.",
"Consumes the iterator.",
);
prov.has_body = true;
index.add_item(req);
index.add_item(prov);
let mut trait_item = make_item_full(
"Iterator",
"mycrate::Iterator",
ItemKind::Trait,
"pub trait Iterator",
"An interface for dealing with iterators.",
"An interface for dealing with iterators.",
);
trait_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Function,
name: "next".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Function,
name: "count".to_string(),
},
];
index.add_item(trait_item);
let di = build_display_item(&index, 2, false, None);
let output = render_json(&di);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("should be valid JSON");
assert_eq!(parsed["kind"], "trait");
let methods = parsed["methods"].as_array().unwrap();
assert_eq!(methods.len(), 2);
assert_eq!(methods[0]["has_body"], false);
assert_eq!(methods[1]["has_body"], true);
insta::assert_snapshot!(output);
}
#[test]
fn render_json_crate_root_includes_top_level_items() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let struct_item = make_item_full(
"Widget",
"mycrate::Widget",
ItemKind::Struct,
"pub struct Widget",
"A widget.",
"A widget.",
);
let mod_item = make_item_full(
"utils",
"mycrate::utils",
ItemKind::Module,
"",
"Utility helpers.",
"Utility helpers.",
);
let fn_item = make_item_full(
"process",
"mycrate::process",
ItemKind::Function,
"pub fn process() -> u32",
"Processes data.",
"Processes data.",
);
index.add_item(struct_item);
index.add_item(mod_item);
index.add_item(fn_item);
let mut crate_item = make_item_full(
"mycrate",
"mycrate",
ItemKind::Module,
"",
"A test crate.",
"A test crate.",
);
crate_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Struct,
name: "Widget".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Module,
name: "utils".to_string(),
},
ChildRef {
index: 2,
kind: ItemKind::Function,
name: "process".to_string(),
},
];
index.add_item(crate_item);
let di = build_display_item(&index, 3, false, None);
let output = render_json(&di);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 4);
let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(first["path"], "mycrate");
assert_eq!(first["kind"], "mod");
assert!(first.get("doc").is_some());
let second: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert!(second.get("summary").is_some());
assert!(second.get("doc").is_none());
insta::assert_snapshot!(output);
}
#[test]
fn render_json_ambiguous_output() {
let item1 = make_item_full(
"Error",
"mycrate::de::Error",
ItemKind::Trait,
"pub trait Error: Sized",
"When deserialization encounters an error.",
"When deserialization encounters an error.",
);
let item2 = make_item_full(
"Error",
"mycrate::ser::Error",
ItemKind::Trait,
"pub trait Error: Sized",
"When serialization encounters an error.",
"When serialization encounters an error.",
);
let items: Vec<&IndexItem> = vec![&item1, &item2];
let output = render_json_ambiguous(&items);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 2);
let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(first["path"], "mycrate::de::Error");
assert_eq!(first["kind"], "trait");
let second: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(second["path"], "mycrate::ser::Error");
insta::assert_snapshot!(output);
}
#[test]
fn render_json_leaf_includes_reexported_from() {
let mut item = make_item_full(
"Helper",
"mycrate::Helper",
ItemKind::Struct,
"pub struct Helper",
"A helper struct.",
"A helper struct.",
);
item.reexport_source = Some("mycrate::inner::Helper".to_string());
let di = DisplayItem::Leaf { item: &item };
let output = render_json(&di);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("should be valid JSON");
assert_eq!(parsed["reexported_from"], "mycrate::inner::Helper");
insta::assert_snapshot!(output);
}
#[test]
fn render_json_leaf_omits_reexported_from_when_none() {
let item = make_item_full(
"Widget",
"mycrate::Widget",
ItemKind::Struct,
"pub struct Widget",
"A widget.",
"A widget.",
);
let di = DisplayItem::Leaf { item: &item };
let output = render_json(&di);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("should be valid JSON");
assert!(
parsed.get("reexported_from").is_none(),
"reexported_from should be omitted when None"
);
}
}