Skip to main content

thread_services/
lib.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
3// SPDX-License-Identifier: AGPL-3.0-or-later
4#![feature(trait_alias)]
5//! # Thread Service Layer
6//!
7//! This crate provides the service layer interfaces for Thread that abstract over
8//! ast-grep functionality while preserving all its powerful capabilities.
9//!
10//! ## Core Philosophy
11//!
12//! The service layer acts as **abstraction glue** that:
13//! - **Preserves Power**: All ast-grep capabilities (Matcher, Replacer, Position) remain accessible
14//! - **Bridges Levels**: Connects file-level AST operations to codebase-level relational intelligence
15//! - **Enables Execution**: Abstracts over different execution environments (rayon, cloud workers)
16//! - **Commercial Ready**: Clear boundaries for commercial extensions
17//!
18//! ## Architecture
19//!
20//! Thread pushes ast-grep from file-level to codebase-level analysis:
21//! - **File Level**: ast-grep provides powerful AST pattern matching and replacement
22//! - **Codebase Level**: Thread adds graph intelligence and cross-file relationships
23//! - **Service Layer**: Abstracts and coordinates both levels seamlessly
24//!
25//! ## Key Components
26//!
27//! - [`types`] - Language-agnostic types that wrap ast-grep functionality
28//! - [`traits`] - Service interfaces for parsing, analysis, and storage
29//! - [`error`] - Comprehensive error handling with recovery strategies
30//! - Execution contexts for different environments (CLI, cloud, WASM)
31//!
32//! ## Examples
33//!
34//! ### Basic Usage - Preserving ast-grep Power
35//! ```rust,no_run
36//! use thread_services::types::ParsedDocument;
37//! use thread_services::traits::CodeAnalyzer;
38//!
39//! async fn analyze_code(document: &ParsedDocument<impl thread_ast_engine::source::Doc>) {
40//!     // Access underlying ast-grep functionality directly
41//!     let root = document.ast_grep_root();
42//!     let matches = root.root().find_all("fn $NAME($$$PARAMS) { $$$BODY }");
43//!
44//!     // Plus codebase-level metadata
45//!     let symbols = document.metadata().defined_symbols.keys();
46//!     println!("Found symbols: {:?}", symbols.collect::<Vec<_>>());
47//! }
48//! ```
49//!
50//! ### Codebase-Level Intelligence
51//! ```rust,no_run
52//! use thread_services::traits::CodeAnalyzer;
53//! use thread_services::types::{AnalysisContext, ExecutionScope};
54//!
55//! async fn codebase_analysis(
56//!     analyzer: &dyn CodeAnalyzer,
57//!     documents: &[thread_services::types::ParsedDocument<impl thread_ast_engine::source::Doc>]
58//! ) -> Result<(), Box<dyn std::error::Error>> {
59//!     let mut context = AnalysisContext::default();
60//!     context.scope = ExecutionScope::Codebase;
61//!
62//!     // Analyze relationships across entire codebase
63//!     let relationships = analyzer.analyze_cross_file_relationships(documents, &context).await?;
64//!
65//!     // This builds on ast-grep's file-level power to create codebase intelligence
66//!     for rel in relationships {
67//!         println!("Cross-file relationship: {:?} -> {:?}", rel.source_file, rel.target_file);
68//!     }
69//!     Ok(())
70//! }
71//! ```
72
73// Core modules
74pub mod conversion;
75pub mod error;
76pub mod facade;
77pub mod traits;
78pub mod types;
79
80// Re-export key types for convenience
81pub use types::{
82    AnalysisContext, AnalysisDepth, CodeMatch, CrossFileRelationship, ExecutionScope,
83    ParsedDocument, SupportLang, SupportLangErr,
84};
85
86pub use error::{
87    AnalysisError, ContextualError, ContextualResult, ErrorContextExt, ParseError,
88    RecoverableError, ServiceError, ServiceResult,
89};
90
91pub use traits::{
92    AnalysisPerformanceProfile, AnalyzerCapabilities, CodeAnalyzer, CodeParser, ParserCapabilities,
93};
94
95#[cfg(feature = "ast-grep-backend")]
96pub use types::{
97    AstNode,
98    AstNodeMatch,
99    // Re-export ast-grep types for compatibility
100    AstPosition,
101    AstRoot,
102};
103
104// Storage traits (commercial boundary)
105#[cfg(feature = "storage-traits")]
106pub use traits::{CacheService, StorageService};
107
108use std::path::Path;
109use thiserror::Error;
110
111/// Legacy error type for backwards compatibility
112#[derive(Error, Debug)]
113#[deprecated(since = "0.1.0", note = "Use ServiceError instead")]
114pub enum LegacyServiceError {
115    #[error("IO error: {0}")]
116    Io(#[from] std::io::Error),
117    #[error("Configuration error: {0}")]
118    Config(String),
119    #[error("Execution error: {0}")]
120    Execution(String),
121}
122
123/// Abstract execution context that can provide code from various sources
124///
125/// This trait provides a generic interface for accessing source code from
126/// different sources (filesystem, memory, network, etc.) to support
127/// different execution environments.
128pub trait ExecutionContext {
129    /// Read content from a source (could be file, memory, network, etc.)
130    fn read_content(&self, source: &str) -> Result<String, ServiceError>;
131
132    /// Write content to a destination
133    fn write_content(&self, destination: &str, content: &str) -> Result<(), ServiceError>;
134
135    /// List available sources (files, URLs, etc.)
136    fn list_sources(&self) -> Result<Vec<String>, ServiceError>;
137}
138
139/// File system based execution context
140pub struct FileSystemContext {
141    base_path: std::path::PathBuf,
142}
143
144impl FileSystemContext {
145    pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
146        Self {
147            base_path: base_path.as_ref().to_path_buf(),
148        }
149    }
150}
151
152impl ExecutionContext for FileSystemContext {
153    fn read_content(&self, source: &str) -> Result<String, ServiceError> {
154        let path = self.base_path.join(source);
155        Ok(std::fs::read_to_string(path)?)
156    }
157
158    fn write_content(&self, destination: &str, content: &str) -> Result<(), ServiceError> {
159        let path = self.base_path.join(destination);
160        if let Some(parent) = path.parent() {
161            std::fs::create_dir_all(parent)?;
162        }
163        Ok(std::fs::write(path, content)?)
164    }
165
166    fn list_sources(&self) -> Result<Vec<String>, ServiceError> {
167        // Basic implementation - can be enhanced with glob patterns, etc.
168        let mut sources = Vec::new();
169        for entry in std::fs::read_dir(&self.base_path)? {
170            let entry = entry?;
171            if entry.file_type()?.is_file()
172                && let Some(name) = entry.file_name().to_str()
173            {
174                sources.push(name.to_string());
175            }
176        }
177        Ok(sources)
178    }
179}
180
181/// In-memory execution context for testing and WASM environments
182pub struct MemoryContext {
183    content: thread_utilities::RapidMap<String, String>,
184}
185
186impl MemoryContext {
187    pub fn new() -> Self {
188        Self {
189            content: thread_utilities::RapidMap::default(),
190        }
191    }
192
193    pub fn add_content(&mut self, name: String, content: String) {
194        self.content.insert(name, content);
195    }
196}
197
198impl Default for MemoryContext {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204impl ExecutionContext for MemoryContext {
205    fn read_content(&self, source: &str) -> Result<String, ServiceError> {
206        self.content
207            .get(source)
208            .cloned()
209            .ok_or_else(|| ServiceError::execution_dynamic(format!("Source not found: {source}")))
210    }
211
212    fn write_content(&self, _destination: &str, _content: &str) -> Result<(), ServiceError> {
213        // For read-only memory context, we could store writes separately
214        // or return an error. For now, we'll just succeed silently.
215        Ok(())
216    }
217
218    fn list_sources(&self) -> Result<Vec<String>, ServiceError> {
219        Ok(self.content.keys().cloned().collect())
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_memory_context() {
229        let mut ctx = MemoryContext::new();
230        ctx.add_content("test.rs".to_string(), "fn main() {}".to_string());
231
232        let content = ctx.read_content("test.rs").unwrap();
233        assert_eq!(content, "fn main() {}");
234
235        let sources = ctx.list_sources().unwrap();
236        assert_eq!(sources, vec!["test.rs"]);
237    }
238}