ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Graph API - Core functionality
//!
//! Provides high-level API for code graph operations.
//! Relationship queries (callers, callees, etc.) are handled by RyoQL.

use super::types::{CodeNode, GraphError, GraphStats};
use crate::Project;
use ryo_analysis::{AnalysisContext, SymbolId, SymbolKind, Visibility};
use std::path::Path;
use std::time::{Duration, Instant};

/// Graph API for code analysis
pub struct GraphApi {
    ctx: AnalysisContext,
    build_time: Duration,
}

impl GraphApi {
    /// Build graph from a project path with workspace resolution.
    pub fn from_path(path: &Path) -> Result<Self, GraphError> {
        let start = Instant::now();
        let project = Project::load(path).map_err(|e| GraphError::LoadFailed(e.to_string()))?;
        Self::from_project(&project).map(|mut api| {
            api.build_time = start.elapsed();
            api
        })
    }

    /// Build graph from a Project with workspace resolution.
    pub fn from_project(project: &Project) -> Result<Self, GraphError> {
        let start = Instant::now();
        let ctx = AnalysisContext::from_workspace_root_parallel(project.workspace_root())
            .map_err(|e| GraphError::LoadFailed(format!("Failed to create context: {}", e)))?;
        Ok(Self {
            ctx,
            build_time: start.elapsed(),
        })
    }

    /// Build graph from an existing AnalysisContext.
    pub fn from_context(ctx: AnalysisContext, build_time: Duration) -> Self {
        Self { ctx, build_time }
    }

    /// Get build time
    pub fn build_time(&self) -> Duration {
        self.build_time
    }

    /// Get graph statistics
    pub fn stats(&self) -> GraphStats {
        let registry = &self.ctx.registry;

        let mut functions = 0;
        let mut structs = 0;
        let mut enums = 0;
        let mut traits = 0;
        let mut impls = 0;

        for (id, _) in registry.iter() {
            if let Some(kind) = registry.kind(id) {
                match kind {
                    SymbolKind::Function => functions += 1,
                    SymbolKind::Struct => structs += 1,
                    SymbolKind::Enum => enums += 1,
                    SymbolKind::Trait => traits += 1,
                    SymbolKind::Impl => impls += 1,
                    _ => {}
                }
            }
        }

        GraphStats {
            files: self.ctx.files.len(),
            nodes: self.ctx.code_graph.node_count(),
            functions,
            structs,
            enums,
            traits,
            impls,
            edges: self.ctx.code_graph.edge_count(),
        }
    }

    /// Get the underlying analysis context
    pub fn context(&self) -> &AnalysisContext {
        &self.ctx
    }

    /// Internal query for summary generation
    pub(crate) fn query_nodes(&self, kind: SymbolKind) -> Vec<CodeNode> {
        self.ctx
            .registry
            .iter()
            .filter(|(id, _)| self.ctx.registry.kind(*id) == Some(kind))
            .filter_map(|(id, _)| self.symbol_to_node(id))
            .collect()
    }

    fn symbol_to_node(&self, id: SymbolId) -> Option<CodeNode> {
        let path = self.ctx.registry.resolve(id)?;
        let kind = self.ctx.registry.kind(id)?;

        let file = self
            .ctx
            .registry
            .span(id)
            .map(|s| s.file.to_absolute())
            .unwrap_or_default();

        let is_public = self
            .ctx
            .registry
            .visibility(id)
            .map(|v| *v == Visibility::Public)
            .unwrap_or(false);

        let is_async = self
            .ctx
            .detail_store
            .function(id)
            .map(|f| f.is_async)
            .unwrap_or(false);

        Some(CodeNode {
            name: path.name().to_string(),
            file,
            kind: kind.into(),
            is_public,
            is_async,
            symbol_id: id,
        })
    }
}

#[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(name: &str) -> String {
    format!("Hello, {}!", name)
}

pub struct User {
    pub name: String,
}

pub enum Status {
    Active,
    Inactive,
}

pub trait Greetable {
    fn greet(&self) -> String;
}
"#,
        )
        .unwrap();

        dir
    }

    #[test]
    fn test_from_path() {
        let dir = create_test_project();
        let result = GraphApi::from_path(dir.path());
        assert!(result.is_ok());
    }

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

        assert_eq!(stats.files, 1);
        assert!(stats.nodes > 0);
    }

    #[test]
    fn test_build_time() {
        let dir = create_test_project();
        let api = GraphApi::from_path(dir.path()).unwrap();
        assert!(api.build_time().as_nanos() > 0);
    }
}