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}