cargo-insert-docs 1.6.0

Inserts feature docs into crate docs, and crate docs into README.
use std::{collections::HashMap, fmt, fs};

use cargo_metadata::MetadataCommand;
use expect_test::expect;
use rustdoc_types::{Crate, Id};

use crate::{rustdoc_json, tests::TreeFormatter};

use super::{Tree, Value};

#[test]
fn test_tree() {
    const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");

    let metadata =
        &MetadataCommand::new().manifest_path(format!("{MANIFEST_DIR}/Cargo.toml")).exec().unwrap();

    let package = metadata.packages.iter().find(|p| p.name.as_str() == "test-crate").unwrap();
    let package_target = package.targets.iter().find(|t| t.is_lib()).unwrap();

    let (_, path) = rustdoc_json::generate(rustdoc_json::Options {
        metadata,
        package,
        package_target,
        toolchain: Some("nightly-2026-04-14"),
        all_features: false,
        no_default_features: false,
        features: &mut None.into_iter(),
        manifest_path: None,
        target: None,
        target_dir: None,
        quiet: false,
        document_private_items: false,
        no_deps: false,
        output: rustdoc_json::CommandOutput::Inherit,
    })
    .unwrap();

    let json = fs::read_to_string(path).expect("failed to read generated rustdoc json");
    let krate: Crate = serde_json::from_str(&json).expect("failed to parse generated rustdoc json");
    let tree = Tree::new(&krate).unwrap();

    expect![[r#"
        test_crate Module
        ├── MY_CONSTANT Constant
        ├── MY_STATIC Static
        ├── MyEnum Enum
        │   └── MyVariant Variant
        ├── MyExternType ExternType
        ├── MyGlobImportedStructFromPrivateMod Struct
        ├── MyInlineGlobImportedStruct Struct
        ├── MyStruct Struct
        │   ├── Error AssocType
        │   ├── Error AssocType
        │   ├── borrow Method
        │   ├── borrow_mut Method
        │   ├── from Method
        │   ├── into Method
        │   ├── my_field StructField
        │   ├── my_method Method
        │   ├── try_from Method
        │   ├── try_into Method
        │   └── type_id Method
        ├── MyStructAlias TypeAlias
        ├── MyTrait Trait
        │   ├── MY_ASSOCIATED_CONSTANT AssocConst
        │   ├── MyAssociatedType AssocType
        │   ├── my_provided_method Method
        │   └── my_required_method TyMethod
        ├── MyTraitAlias TraitAlias
        ├── MyUnion Union
        ├── ReexportInline Struct
        ├── ReexportPrivate Struct
        ├── my_function Function
        ├── my_glob_imported_fn_from_private_mod Function
        ├── my_inline_glob_imported_fn Function
        ├── my_macro Macro
        ├── my_module Module
        ├── reexport Module
        │   └── Reexport Struct
        ├── reexport_inline Module
        ├── to_be_glob_imported Module
        │   ├── MyGlobImportedStruct Struct
        │   └── my_glob_imported_fn Function
        ├── to_be_inline_glob_imported Module
        └── very Module
            └── nested Module
                └── module Module
        to_be_glob_imported_private Module
    "#]]
    .assert_eq(&tree.to_string());
}

impl fmt::Display for Tree<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&format_tree(self))
    }
}

fn format_tree(tree: &Tree) -> String {
    let mut branches: HashMap<Id, Branch> = HashMap::new();

    for (&id, value) in &tree.inv_tree {
        branches.entry(id).or_insert(Branch { value, children: vec![] });

        if let Some(parent_id) = value.parent {
            let value = &tree.inv_tree[&parent_id];
            let branch = branches.entry(parent_id).or_insert(Branch { value, children: vec![] });
            branch.children.push(id);
        }
    }

    for branch in branches.values_mut() {
        branch.children.sort_by_key(|id| tree.inv_tree[id].name);
    }

    let mut roots = tree
        .inv_tree
        .iter()
        .filter_map(|(&i, v)| v.parent.is_none().then_some(i))
        .collect::<Vec<_>>();

    roots.sort_by_key(|id| tree.inv_tree[id].name);

    let mut out = String::new();

    for root in roots {
        out.push_str(&format_branch(&branches, root));
    }

    out
}

fn format_branch(branches: &HashMap<Id, Branch>, id: Id) -> String {
    let mut fmt = TreeFormatter::default();
    let Branch { value: Value { kind, name, .. }, children } = &branches[&id];

    let space = if name.is_empty() { "" } else { " " };
    fmt.label(format_args!("{name}{space}{kind:?}"));
    fmt.children(children.iter().map(|&id| format_branch(branches, id)));
    fmt.finish()
}

struct Branch<'a> {
    value: &'a Value<'a>,
    children: Vec<Id>,
}