1use super::api::GraphApi;
6use ryo_analysis::{SummaryOptions, SymbolKind, ToSummary};
7
8pub struct SummaryBuilder<'a> {
10 api: &'a GraphApi,
11 options: SummaryOptions,
12}
13
14impl<'a> SummaryBuilder<'a> {
15 pub fn new(api: &'a GraphApi) -> Self {
17 Self {
18 api,
19 options: SummaryOptions::default(),
20 }
21 }
22
23 pub fn detailed(mut self) -> Self {
25 self.options = SummaryOptions::detailed();
26 self
27 }
28
29 pub fn compact(mut self) -> Self {
31 self.options = SummaryOptions::compact();
32 self
33 }
34
35 pub fn max_items(mut self, max: usize) -> Self {
37 self.options = self.options.with_max_depth(max);
38 self
39 }
40
41 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 out.push_str("=== CodeGraph ===\n");
54
55 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 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 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 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 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}