Skip to main content

ryo_app/graph/
api.rs

1//! Graph API - Core functionality
2//!
3//! Provides high-level API for code graph operations.
4//! Relationship queries (callers, callees, etc.) are handled by RyoQL.
5
6use super::types::{CodeNode, GraphError, GraphStats};
7use crate::Project;
8use ryo_analysis::{AnalysisContext, SymbolId, SymbolKind, Visibility};
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12/// Graph API for code analysis
13pub struct GraphApi {
14    ctx: AnalysisContext,
15    build_time: Duration,
16}
17
18impl GraphApi {
19    /// Build graph from a project path with workspace resolution.
20    pub fn from_path(path: &Path) -> Result<Self, GraphError> {
21        let start = Instant::now();
22        let project = Project::load(path).map_err(|e| GraphError::LoadFailed(e.to_string()))?;
23        Self::from_project(&project).map(|mut api| {
24            api.build_time = start.elapsed();
25            api
26        })
27    }
28
29    /// Build graph from a Project with workspace resolution.
30    pub fn from_project(project: &Project) -> Result<Self, GraphError> {
31        let start = Instant::now();
32        let ctx = AnalysisContext::from_workspace_root_parallel(project.workspace_root())
33            .map_err(|e| GraphError::LoadFailed(format!("Failed to create context: {}", e)))?;
34        Ok(Self {
35            ctx,
36            build_time: start.elapsed(),
37        })
38    }
39
40    /// Build graph from an existing AnalysisContext.
41    pub fn from_context(ctx: AnalysisContext, build_time: Duration) -> Self {
42        Self { ctx, build_time }
43    }
44
45    /// Get build time
46    pub fn build_time(&self) -> Duration {
47        self.build_time
48    }
49
50    /// Get graph statistics
51    pub fn stats(&self) -> GraphStats {
52        let registry = &self.ctx.registry;
53
54        let mut functions = 0;
55        let mut structs = 0;
56        let mut enums = 0;
57        let mut traits = 0;
58        let mut impls = 0;
59
60        for (id, _) in registry.iter() {
61            if let Some(kind) = registry.kind(id) {
62                match kind {
63                    SymbolKind::Function => functions += 1,
64                    SymbolKind::Struct => structs += 1,
65                    SymbolKind::Enum => enums += 1,
66                    SymbolKind::Trait => traits += 1,
67                    SymbolKind::Impl => impls += 1,
68                    _ => {}
69                }
70            }
71        }
72
73        GraphStats {
74            files: self.ctx.files.len(),
75            nodes: self.ctx.code_graph.node_count(),
76            functions,
77            structs,
78            enums,
79            traits,
80            impls,
81            edges: self.ctx.code_graph.edge_count(),
82        }
83    }
84
85    /// Get the underlying analysis context
86    pub fn context(&self) -> &AnalysisContext {
87        &self.ctx
88    }
89
90    /// Internal query for summary generation
91    pub(crate) fn query_nodes(&self, kind: SymbolKind) -> Vec<CodeNode> {
92        self.ctx
93            .registry
94            .iter()
95            .filter(|(id, _)| self.ctx.registry.kind(*id) == Some(kind))
96            .filter_map(|(id, _)| self.symbol_to_node(id))
97            .collect()
98    }
99
100    fn symbol_to_node(&self, id: SymbolId) -> Option<CodeNode> {
101        let path = self.ctx.registry.resolve(id)?;
102        let kind = self.ctx.registry.kind(id)?;
103
104        let file = self
105            .ctx
106            .registry
107            .span(id)
108            .map(|s| s.file.to_absolute())
109            .unwrap_or_default();
110
111        let is_public = self
112            .ctx
113            .registry
114            .visibility(id)
115            .map(|v| *v == Visibility::Public)
116            .unwrap_or(false);
117
118        let is_async = self
119            .ctx
120            .detail_store
121            .function(id)
122            .map(|f| f.is_async)
123            .unwrap_or(false);
124
125        Some(CodeNode {
126            name: path.name().to_string(),
127            file,
128            kind: kind.into(),
129            is_public,
130            is_async,
131            symbol_id: id,
132        })
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::fs;
140    use tempfile::tempdir;
141
142    fn create_test_project() -> tempfile::TempDir {
143        let dir = tempdir().unwrap();
144        let src = dir.path().join("src");
145        fs::create_dir(&src).unwrap();
146
147        fs::write(
148            dir.path().join("Cargo.toml"),
149            r#"[package]
150name = "test-project"
151version = "0.1.0"
152edition = "2021"
153"#,
154        )
155        .unwrap();
156
157        fs::write(
158            src.join("lib.rs"),
159            r#"
160pub fn greet(name: &str) -> String {
161    format!("Hello, {}!", name)
162}
163
164pub struct User {
165    pub name: String,
166}
167
168pub enum Status {
169    Active,
170    Inactive,
171}
172
173pub trait Greetable {
174    fn greet(&self) -> String;
175}
176"#,
177        )
178        .unwrap();
179
180        dir
181    }
182
183    #[test]
184    fn test_from_path() {
185        let dir = create_test_project();
186        let result = GraphApi::from_path(dir.path());
187        assert!(result.is_ok());
188    }
189
190    #[test]
191    fn test_stats() {
192        let dir = create_test_project();
193        let api = GraphApi::from_path(dir.path()).unwrap();
194        let stats = api.stats();
195
196        assert_eq!(stats.files, 1);
197        assert!(stats.nodes > 0);
198    }
199
200    #[test]
201    fn test_build_time() {
202        let dir = create_test_project();
203        let api = GraphApi::from_path(dir.path()).unwrap();
204        assert!(api.build_time().as_nanos() > 0);
205    }
206}