cargo-docs-md 0.2.4

Generate per-module markdown documentation from rustdoc JSON output
Documentation
//! Rustdoc JSON parsing module.
//!
//! This module handles loading and parsing rustdoc JSON files into the
//! `rustdoc_types::Crate` structure that represents the entire documented crate.
//!
//! # Rustdoc JSON Format
//!
//! Rustdoc JSON is generated by running:
//! ```bash
//! cargo doc --output-format json
//! ```
//!
//! The output is a single JSON file at `target/doc/{crate_name}.json` containing:
//! - The crate's module hierarchy
//! - All public (and optionally private) items
//! - Documentation strings
//! - Type information and generics
//! - Cross-reference links between items
//!
//! # Key Types
//!
//! The parsed `Crate` contains:
//! - `root`: ID of the root module
//! - `index`: `HashMap` of all items by their ID
//! - `paths`: `HashMap` mapping IDs to their full module paths
//! - `crate_version`: Optional version string
//!
//! # Performance
//!
//! When the `simd-json` feature is enabled, parsing uses SIMD-accelerated
//! JSON parsing which is significantly faster for large rustdoc JSON files
//! (10-50MB+). This requires AVX2/SSE4.2 on x86 platforms.
//!
//! # Memory Usage
//!
//! The entire rustdoc JSON file is loaded into memory and deserialized into
//! a `Crate` structure. For typical crates (1-20MB JSON), this works well.
//!
//! For very large crates (e.g., `aws_sdk_ec2` at ~500MB), memory usage will be:
//! - JSON file size in memory during parsing
//! - Plus the deserialized `Crate` structure (usually similar size)
//! - Peak memory ≈ 2x JSON file size
//!
//! Future optimization: For extremely large crates, `serde_json::StreamDeserializer`
//! could be used for incremental parsing, trading some simplicity for lower peak memory.

use fs_err as FileSystemError;
use rustdoc_types::Crate;
use tracing::instrument;

use crate::error::Error;

/// Parser for rustdoc JSON files.
///
/// Provides methods to load and parse rustdoc JSON from files or strings
/// into the `rustdoc_types::Crate` structure.
pub struct Parser;

impl Parser {
    /// Parse a rustdoc JSON file from disk into a `Crate` structure.
    ///
    /// This is the primary entry point for loading documentation data.
    /// The file should be generated with `cargo doc --output-format json`.
    /// Parse a JSON string into a Crate structure.
    ///
    /// # Errors
    /// Returns an error if it faills to parse the JSON.
    #[instrument(skip(json), fields(json_len = json.len()))]
    pub fn parse_json(json: &str) -> Result<Crate, Error> {
        tracing::info!("Starting JSON parsing");

        #[cfg(feature = "simd-json")]
        let result = {
            tracing::debug!("Using simd-json parser");

            let mut json_bytes = json.as_bytes().to_vec();
            simd_json::from_slice::<Crate>(&mut json_bytes).map_err(Error::SimdJsonParse)
        };

        #[cfg(not(feature = "simd-json"))]
        let result: Result<Crate, Error> = {
            tracing::debug!("Using serde_json parser");

            serde_json::from_str(json).map_err(Error::JsonParse)
        };

        match &result {
            Ok(krate) => {
                tracing::info!(
                    crate_name = ?krate.index.get(&krate.root).and_then(|i| i.name.as_ref()),
                    item_count = krate.index.len(),
                    "Successfully parsed crate"
                );
            },

            Err(e) => {
                tracing::warn!(error = %e, "Failed to parse JSON");
            },
        }

        result
    }

    /// Parse a JSON file.
    ///
    /// # Errors
    /// Returns error if fails to read the JSON file.
    #[instrument(skip_all, fields(path = %path.as_ref().display()))]
    pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Crate, Error> {
        let path = path.as_ref();
        tracing::debug!("Reading file");

        let json = FileSystemError::read_to_string(path).map_err(Error::FileRead)?;
        tracing::debug!(bytes = json.len(), "File read successfully");

        Self::parse_json(&json)
    }

    /// Parse a rustdoc JSON string into a `Crate` structure.
    ///
    /// This is an alias for [`parse_json`] provided for API clarity when the
    /// caller wants to emphasize that they're parsing string content rather
    /// than a file path.
    ///
    /// # Arguments
    ///
    /// * `content` - The raw JSON string to parse
    ///
    /// # Returns
    ///
    /// A parsed `Crate` structure containing all documentation data.
    ///
    /// # Errors
    ///
    /// Returns `Error::JsonParse` if the JSON is invalid or doesn't match
    /// the expected rustdoc JSON schema.
    ///
    /// # Schema Compatibility
    ///
    /// The `rustdoc-types` crate version must match the rustdoc JSON format
    /// version. Mismatches can cause parsing failures or missing fields.
    #[inline]
    pub fn parse_json_string(content: &str) -> Result<Crate, Error> {
        // Delegate to parse_json to ensure consistent behavior:
        // - Feature gates (simd-json when enabled)
        // - Tracing/logging
        // - Error handling
        Self::parse_json(content)
    }
}