ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Summary generation for Graph API
//!
//! Provides AI-readable summary of the code graph.

use super::api::GraphApi;
use ryo_analysis::{SummaryOptions, SymbolKind, ToSummary};

/// Builder for generating graph summaries
pub struct SummaryBuilder<'a> {
    api: &'a GraphApi,
    options: SummaryOptions,
}

impl<'a> SummaryBuilder<'a> {
    /// Create a new summary builder
    pub fn new(api: &'a GraphApi) -> Self {
        Self {
            api,
            options: SummaryOptions::default(),
        }
    }

    /// Use detailed options
    pub fn detailed(mut self) -> Self {
        self.options = SummaryOptions::detailed();
        self
    }

    /// Use compact options
    pub fn compact(mut self) -> Self {
        self.options = SummaryOptions::compact();
        self
    }

    /// Set max items per section
    pub fn max_items(mut self, max: usize) -> Self {
        self.options = self.options.with_max_depth(max);
        self
    }

    /// Build the summary string
    pub fn build(self) -> String {
        self.api.to_summary_with_options(&self.options)
    }
}

impl ToSummary for GraphApi {
    fn to_summary_with_options(&self, opts: &SummaryOptions) -> String {
        let mut out = String::new();
        let stats = self.stats();

        // Header
        out.push_str("=== CodeGraph ===\n");

        // Stats
        if opts.include_stats {
            out.push_str(&format!("Nodes: {}\n", stats.nodes));
            out.push_str(&format!("Edges: {}\n", stats.edges));
            out.push_str(&format!(
                "Types: {} functions, {} structs, {} enums, {} traits, {} impls\n",
                stats.functions, stats.structs, stats.enums, stats.traits, stats.impls
            ));
            out.push_str(&format!("Files: {}\n", stats.files));
        } else if !opts.compact {
            out.push_str(&format!(
                "Nodes: {}, Edges: {}, Files: {}\n",
                stats.nodes, stats.edges, stats.files
            ));
        }

        out.push('\n');

        let max_items = opts.max_depth.unwrap_or(50);

        // Functions section
        let functions = self.query_nodes(SymbolKind::Function);
        if !functions.is_empty() {
            out.push_str("[Functions]\n");
            for node in functions.iter().take(max_items) {
                let vis = if node.is_public { "pub " } else { "" };
                if opts.compact {
                    out.push_str(&format!("  {}fn {}\n", vis, node.name));
                } else if opts.include_locations {
                    out.push_str(&format!(
                        "  {}fn {} @ {} [{:?}]\n",
                        vis,
                        node.name,
                        node.file.display(),
                        node.symbol_id
                    ));
                } else {
                    out.push_str(&format!(
                        "  {}fn {} [{:?}]\n",
                        vis, node.name, node.symbol_id
                    ));
                }
            }
            if functions.len() > max_items {
                out.push_str(&format!("  ... and {} more\n", functions.len() - max_items));
            }
            out.push('\n');
        }

        // Structs section
        let structs = self.query_nodes(SymbolKind::Struct);
        if !structs.is_empty() {
            out.push_str("[Structs]\n");
            for node in structs.iter().take(max_items) {
                let vis = if node.is_public { "pub " } else { "" };
                if opts.compact {
                    out.push_str(&format!("  {}struct {}\n", vis, node.name));
                } else if opts.include_locations {
                    out.push_str(&format!(
                        "  {}struct {} @ {} [{:?}]\n",
                        vis,
                        node.name,
                        node.file.display(),
                        node.symbol_id
                    ));
                } else {
                    out.push_str(&format!(
                        "  {}struct {} [{:?}]\n",
                        vis, node.name, node.symbol_id
                    ));
                }
            }
            if structs.len() > max_items {
                out.push_str(&format!("  ... and {} more\n", structs.len() - max_items));
            }
            out.push('\n');
        }

        // Enums section
        let enums = self.query_nodes(SymbolKind::Enum);
        if !enums.is_empty() {
            out.push_str("[Enums]\n");
            for node in enums.iter().take(max_items) {
                let vis = if node.is_public { "pub " } else { "" };
                if opts.compact {
                    out.push_str(&format!("  {}enum {}\n", vis, node.name));
                } else {
                    out.push_str(&format!(
                        "  {}enum {} [{:?}]\n",
                        vis, node.name, node.symbol_id
                    ));
                }
            }
            out.push('\n');
        }

        // Traits section
        let traits = self.query_nodes(SymbolKind::Trait);
        if !traits.is_empty() {
            out.push_str("[Traits]\n");
            for node in traits.iter().take(max_items) {
                let vis = if node.is_public { "pub " } else { "" };
                if opts.compact {
                    out.push_str(&format!("  {}trait {}\n", vis, node.name));
                } else {
                    out.push_str(&format!(
                        "  {}trait {} [{:?}]\n",
                        vis, node.name, node.symbol_id
                    ));
                }
            }
            out.push('\n');
        }

        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    fn create_test_project() -> tempfile::TempDir {
        let dir = tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir(&src).unwrap();

        fs::write(
            dir.path().join("Cargo.toml"),
            r#"[package]
name = "test-project"
version = "0.1.0"
edition = "2021"
"#,
        )
        .unwrap();

        fs::write(
            src.join("lib.rs"),
            r#"
pub fn greet() {}
pub struct User {}
pub enum Status { Active }
pub trait Greetable {}
"#,
        )
        .unwrap();

        dir
    }

    #[test]
    fn test_to_summary() {
        let dir = create_test_project();
        let api = GraphApi::from_path(dir.path()).unwrap();

        let summary = SummaryBuilder::new(&api).build();

        assert!(summary.contains("CodeGraph"));
        assert!(summary.contains("Nodes"));
    }

    #[test]
    fn test_summary_detailed() {
        let dir = create_test_project();
        let api = GraphApi::from_path(dir.path()).unwrap();

        let summary = SummaryBuilder::new(&api).detailed().build();

        assert!(summary.contains("functions"));
        assert!(summary.contains("structs"));
    }

    #[test]
    fn test_summary_compact() {
        let dir = create_test_project();
        let api = GraphApi::from_path(dir.path()).unwrap();

        let summary = SummaryBuilder::new(&api).compact().build();
        assert!(summary.contains("CodeGraph"));
    }
}