Skip to main content

ryo_app/graph/
summary.rs

1//! Summary generation for Graph API
2//!
3//! Provides AI-readable summary of the code graph.
4
5use super::api::GraphApi;
6use ryo_analysis::{SummaryOptions, SymbolKind, ToSummary};
7
8/// Builder for generating graph summaries
9pub struct SummaryBuilder<'a> {
10    api: &'a GraphApi,
11    options: SummaryOptions,
12}
13
14impl<'a> SummaryBuilder<'a> {
15    /// Create a new summary builder
16    pub fn new(api: &'a GraphApi) -> Self {
17        Self {
18            api,
19            options: SummaryOptions::default(),
20        }
21    }
22
23    /// Use detailed options
24    pub fn detailed(mut self) -> Self {
25        self.options = SummaryOptions::detailed();
26        self
27    }
28
29    /// Use compact options
30    pub fn compact(mut self) -> Self {
31        self.options = SummaryOptions::compact();
32        self
33    }
34
35    /// Set max items per section
36    pub fn max_items(mut self, max: usize) -> Self {
37        self.options = self.options.with_max_depth(max);
38        self
39    }
40
41    /// Build the summary string
42    pub fn build(self) -> String {
43        self.api.to_summary_with_options(&self.options)
44    }
45}
46
47impl ToSummary for GraphApi {
48    fn to_summary_with_options(&self, opts: &SummaryOptions) -> String {
49        let mut out = String::new();
50        let stats = self.stats();
51
52        // Header
53        out.push_str("=== CodeGraph ===\n");
54
55        // Stats
56        if opts.include_stats {
57            out.push_str(&format!("Nodes: {}\n", stats.nodes));
58            out.push_str(&format!("Edges: {}\n", stats.edges));
59            out.push_str(&format!(
60                "Types: {} functions, {} structs, {} enums, {} traits, {} impls\n",
61                stats.functions, stats.structs, stats.enums, stats.traits, stats.impls
62            ));
63            out.push_str(&format!("Files: {}\n", stats.files));
64        } else if !opts.compact {
65            out.push_str(&format!(
66                "Nodes: {}, Edges: {}, Files: {}\n",
67                stats.nodes, stats.edges, stats.files
68            ));
69        }
70
71        out.push('\n');
72
73        let max_items = opts.max_depth.unwrap_or(50);
74
75        // Functions section
76        let functions = self.query_nodes(SymbolKind::Function);
77        if !functions.is_empty() {
78            out.push_str("[Functions]\n");
79            for node in functions.iter().take(max_items) {
80                let vis = if node.is_public { "pub " } else { "" };
81                if opts.compact {
82                    out.push_str(&format!("  {}fn {}\n", vis, node.name));
83                } else if opts.include_locations {
84                    out.push_str(&format!(
85                        "  {}fn {} @ {} [{:?}]\n",
86                        vis,
87                        node.name,
88                        node.file.display(),
89                        node.symbol_id
90                    ));
91                } else {
92                    out.push_str(&format!(
93                        "  {}fn {} [{:?}]\n",
94                        vis, node.name, node.symbol_id
95                    ));
96                }
97            }
98            if functions.len() > max_items {
99                out.push_str(&format!("  ... and {} more\n", functions.len() - max_items));
100            }
101            out.push('\n');
102        }
103
104        // Structs section
105        let structs = self.query_nodes(SymbolKind::Struct);
106        if !structs.is_empty() {
107            out.push_str("[Structs]\n");
108            for node in structs.iter().take(max_items) {
109                let vis = if node.is_public { "pub " } else { "" };
110                if opts.compact {
111                    out.push_str(&format!("  {}struct {}\n", vis, node.name));
112                } else if opts.include_locations {
113                    out.push_str(&format!(
114                        "  {}struct {} @ {} [{:?}]\n",
115                        vis,
116                        node.name,
117                        node.file.display(),
118                        node.symbol_id
119                    ));
120                } else {
121                    out.push_str(&format!(
122                        "  {}struct {} [{:?}]\n",
123                        vis, node.name, node.symbol_id
124                    ));
125                }
126            }
127            if structs.len() > max_items {
128                out.push_str(&format!("  ... and {} more\n", structs.len() - max_items));
129            }
130            out.push('\n');
131        }
132
133        // Enums section
134        let enums = self.query_nodes(SymbolKind::Enum);
135        if !enums.is_empty() {
136            out.push_str("[Enums]\n");
137            for node in enums.iter().take(max_items) {
138                let vis = if node.is_public { "pub " } else { "" };
139                if opts.compact {
140                    out.push_str(&format!("  {}enum {}\n", vis, node.name));
141                } else {
142                    out.push_str(&format!(
143                        "  {}enum {} [{:?}]\n",
144                        vis, node.name, node.symbol_id
145                    ));
146                }
147            }
148            out.push('\n');
149        }
150
151        // Traits section
152        let traits = self.query_nodes(SymbolKind::Trait);
153        if !traits.is_empty() {
154            out.push_str("[Traits]\n");
155            for node in traits.iter().take(max_items) {
156                let vis = if node.is_public { "pub " } else { "" };
157                if opts.compact {
158                    out.push_str(&format!("  {}trait {}\n", vis, node.name));
159                } else {
160                    out.push_str(&format!(
161                        "  {}trait {} [{:?}]\n",
162                        vis, node.name, node.symbol_id
163                    ));
164                }
165            }
166            out.push('\n');
167        }
168
169        out
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::fs;
177    use tempfile::tempdir;
178
179    fn create_test_project() -> tempfile::TempDir {
180        let dir = tempdir().unwrap();
181        let src = dir.path().join("src");
182        fs::create_dir(&src).unwrap();
183
184        fs::write(
185            dir.path().join("Cargo.toml"),
186            r#"[package]
187name = "test-project"
188version = "0.1.0"
189edition = "2021"
190"#,
191        )
192        .unwrap();
193
194        fs::write(
195            src.join("lib.rs"),
196            r#"
197pub fn greet() {}
198pub struct User {}
199pub enum Status { Active }
200pub trait Greetable {}
201"#,
202        )
203        .unwrap();
204
205        dir
206    }
207
208    #[test]
209    fn test_to_summary() {
210        let dir = create_test_project();
211        let api = GraphApi::from_path(dir.path()).unwrap();
212
213        let summary = SummaryBuilder::new(&api).build();
214
215        assert!(summary.contains("CodeGraph"));
216        assert!(summary.contains("Nodes"));
217    }
218
219    #[test]
220    fn test_summary_detailed() {
221        let dir = create_test_project();
222        let api = GraphApi::from_path(dir.path()).unwrap();
223
224        let summary = SummaryBuilder::new(&api).detailed().build();
225
226        assert!(summary.contains("functions"));
227        assert!(summary.contains("structs"));
228    }
229
230    #[test]
231    fn test_summary_compact() {
232        let dir = create_test_project();
233        let api = GraphApi::from_path(dir.path()).unwrap();
234
235        let summary = SummaryBuilder::new(&api).compact().build();
236        assert!(summary.contains("CodeGraph"));
237    }
238}