1pub mod error;
33pub mod types;
34
35pub mod storage;
37pub mod graph;
38pub mod search;
39pub mod cfg;
40pub mod edit;
41pub mod analysis;
42
43pub use sqlitegraph::backend::{NodeSpec, EdgeSpec};
53pub use sqlitegraph::graph::{GraphEntity, SqliteGraph};
54pub use sqlitegraph::config::{BackendKind as SqliteGraphBackendKind, GraphConfig, open_graph};
55
56pub use error::{ForgeError, Result};
58pub use types::{SymbolId, Location};
59pub use storage::{BackendKind, UnifiedGraphStore};
60
61use anyhow::anyhow;
62
63#[derive(Clone, Debug)]
68pub struct Forge {
69 store: std::sync::Arc<UnifiedGraphStore>,
70}
71
72impl Forge {
73 pub async fn open(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
86 Self::open_with_backend(path, BackendKind::default()).await
87 }
88
89 pub async fn open_with_backend(
100 path: impl AsRef<std::path::Path>,
101 backend: BackendKind
102 ) -> anyhow::Result<Self> {
103 let store = std::sync::Arc::new(
104 storage::UnifiedGraphStore::open(path, backend).await?
105 );
106 Ok(Forge { store })
107 }
108
109 pub fn backend_kind(&self) -> BackendKind {
111 self.store.backend_kind()
112 }
113
114 pub fn graph(&self) -> graph::GraphModule {
116 graph::GraphModule::new(self.store.clone())
117 }
118
119 pub fn search(&self) -> search::SearchModule {
121 search::SearchModule::new(self.store.clone())
122 }
123
124 pub fn cfg(&self) -> cfg::CfgModule {
126 cfg::CfgModule::new(self.store.clone())
127 }
128
129 pub fn edit(&self) -> edit::EditModule {
131 edit::EditModule::new(self.store.clone())
132 }
133
134 pub fn analysis(&self) -> analysis::AnalysisModule {
136 analysis::AnalysisModule::new(
137 self.graph(),
138 self.cfg(),
139 self.edit(),
140 )
141 }
142}
143
144#[derive(Clone, Default)]
146pub struct ForgeBuilder {
147 path: Option<std::path::PathBuf>,
148 database_path: Option<std::path::PathBuf>,
149 backend_kind: Option<BackendKind>,
150 cache_ttl: Option<std::time::Duration>,
151}
152
153impl ForgeBuilder {
154 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn path(mut self, path: impl AsRef<std::path::Path>) -> Self {
161 Self {
162 path: Some(path.as_ref().to_path_buf()),
163 ..self
164 }
165 }
166
167 pub fn database_path(mut self, db_path: impl AsRef<std::path::Path>) -> Self {
169 Self {
170 database_path: Some(db_path.as_ref().to_path_buf()),
171 ..self
172 }
173 }
174
175 pub fn backend_kind(mut self, kind: BackendKind) -> Self {
177 Self {
178 backend_kind: Some(kind),
179 ..self
180 }
181 }
182
183 pub fn cache_ttl(mut self, ttl: std::time::Duration) -> Self {
185 Self {
186 cache_ttl: Some(ttl),
187 ..self
188 }
189 }
190
191 pub async fn build(self) -> anyhow::Result<Forge> {
193 let path = self.path
194 .ok_or_else(|| anyhow!("path is required"))?;
195
196 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(
197 &path,
198 self.backend_kind.unwrap_or_default()
199 ).await?);
200
201 Ok(Forge { store })
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[tokio::test]
212 async fn test_forge_open_creates_database() {
213 let temp_dir = tempfile::tempdir().unwrap();
214 let db_path = temp_dir.path().join(".forge").join("graph.db");
215
216 assert!(!db_path.exists());
218
219 let forge = Forge::open(temp_dir.path()).await.unwrap();
221
222 assert!(db_path.exists());
224
225 let _graph = forge.graph();
227 let _search = forge.search();
228
229 drop(forge);
230 }
231
232 #[tokio::test]
235 async fn test_forge_graph_accessor() {
236 let temp_dir = tempfile::tempdir().unwrap();
237 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
238
239 let forge = Forge { store };
240
241 let graph = forge.graph();
243 drop(graph);
244 }
245
246 #[tokio::test]
247 async fn test_forge_search_accessor() {
248 let temp_dir = tempfile::tempdir().unwrap();
249 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
250
251 let forge = Forge { store };
252
253 let search = forge.search();
255 drop(search);
256 }
257
258 #[tokio::test]
259 async fn test_forge_cfg_accessor() {
260 let temp_dir = tempfile::tempdir().unwrap();
261 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
262
263 let forge = Forge { store };
264
265 let cfg = forge.cfg();
267 drop(cfg);
268 }
269
270 #[tokio::test]
271 async fn test_forge_edit_accessor() {
272 let temp_dir = tempfile::tempdir().unwrap();
273 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
274
275 let forge = Forge { store };
276
277 let edit = forge.edit();
279 drop(edit);
280 }
281
282 #[tokio::test]
283 async fn test_forge_analysis_accessor() {
284 let temp_dir = tempfile::tempdir().unwrap();
285 let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
286
287 let forge = Forge { store };
288
289 let analysis = forge.analysis();
291 drop(analysis);
292 }
293
294 #[test]
297 fn test_forge_builder_default() {
298 let builder = ForgeBuilder::new();
299
300 assert!(builder.path.is_none());
302 assert!(builder.database_path.is_none());
303 assert!(builder.backend_kind.is_none());
304 assert!(builder.cache_ttl.is_none());
305 }
306
307 #[test]
308 fn test_forge_builder_path() {
309 let temp_dir = tempfile::tempdir().unwrap();
310 let path = temp_dir.path().join("test");
311 let builder = ForgeBuilder::new().path(&path);
312
313 assert_eq!(builder.path, Some(std::path::PathBuf::from(path)));
314 }
315
316 #[test]
317 fn test_forge_builder_database_path() {
318 let builder = ForgeBuilder::new().database_path("custom.db");
319
320 assert_eq!(builder.database_path, Some(std::path::PathBuf::from("custom.db")));
321 }
322
323 #[test]
324 fn test_forge_builder_backend_kind() {
325 let builder = ForgeBuilder::new().backend_kind(BackendKind::NativeV3);
326
327 assert_eq!(builder.backend_kind, Some(BackendKind::NativeV3));
328 }
329
330 #[test]
331 fn test_forge_builder_cache_ttl() {
332 let ttl = std::time::Duration::from_secs(60);
333 let builder = ForgeBuilder::new().cache_ttl(ttl);
334
335 assert_eq!(builder.cache_ttl, Some(ttl));
336 }
337
338 #[tokio::test]
339 async fn test_forge_builder_build_success() {
340 let temp_dir = tempfile::tempdir().unwrap();
341 let builder = ForgeBuilder::new()
342 .path(temp_dir.path())
343 .backend_kind(BackendKind::SQLite);
344
345 let forge = builder.build().await.unwrap();
346
347 assert!(forge.store.is_connected());
348 }
349
350 #[tokio::test]
351 async fn test_forge_builder_missing_path() {
352 let builder = ForgeBuilder::new();
353
354 let result = builder.build().await;
355
356 assert!(result.is_err());
357 assert!(result.unwrap_err().to_string().contains("path"));
358 }
359}