1use crate::colony::Colony;
35use crate::topology_impl::PetTopologyGraph;
36use phago_core::topology::TopologyGraph;
37use phago_core::types::*;
38use std::path::{Path, PathBuf};
39
40#[cfg(feature = "sqlite")]
41use crate::sqlite_topology::SqliteTopologyGraph;
42
43#[derive(Debug)]
45pub enum BuilderError {
46 SqliteNotEnabled,
48 DatabaseError(String),
50 PersistenceError(String),
52}
53
54impl std::fmt::Display for BuilderError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 BuilderError::SqliteNotEnabled => {
58 write!(f, "SQLite feature not enabled. Add features = [\"sqlite\"] to Cargo.toml")
59 }
60 BuilderError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
61 BuilderError::PersistenceError(msg) => write!(f, "Persistence error: {}", msg),
62 }
63 }
64}
65
66impl std::error::Error for BuilderError {}
67
68pub struct ColonyBuilder {
70 persistence_path: Option<PathBuf>,
71 auto_save: bool,
72 cache_size: usize,
73}
74
75impl Default for ColonyBuilder {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl ColonyBuilder {
82 pub fn new() -> Self {
84 Self {
85 persistence_path: None,
86 auto_save: false,
87 cache_size: 1000,
88 }
89 }
90
91 #[cfg(feature = "sqlite")]
96 pub fn with_persistence<P: AsRef<Path>>(mut self, path: P) -> Self {
97 self.persistence_path = Some(path.as_ref().to_path_buf());
98 self
99 }
100
101 #[cfg(not(feature = "sqlite"))]
103 pub fn with_persistence<P: AsRef<Path>>(self, _path: P) -> Self {
104 self
106 }
107
108 pub fn auto_save(mut self, enabled: bool) -> Self {
113 self.auto_save = enabled;
114 self
115 }
116
117 pub fn cache_size(mut self, size: usize) -> Self {
119 self.cache_size = size;
120 self
121 }
122
123 pub fn build_simple(self) -> Colony {
125 Colony::new()
126 }
127
128 #[cfg(feature = "sqlite")]
130 pub fn build(self) -> Result<PersistentColony, BuilderError> {
131 let mut colony = Colony::new();
132
133 let persistence = if let Some(path) = self.persistence_path {
134 let db = SqliteTopologyGraph::open(&path)
135 .map_err(|e| BuilderError::DatabaseError(e.to_string()))?
136 .with_cache_size(self.cache_size);
137
138 load_from_sqlite(&db, colony.substrate_mut().graph_mut())?;
140
141 Some(PersistenceState {
142 db,
143 path,
144 auto_save: self.auto_save,
145 })
146 } else {
147 None
148 };
149
150 Ok(PersistentColony {
151 colony,
152 persistence,
153 })
154 }
155
156 #[cfg(not(feature = "sqlite"))]
158 pub fn build(self) -> Result<PersistentColony, BuilderError> {
159 if self.persistence_path.is_some() {
160 return Err(BuilderError::SqliteNotEnabled);
161 }
162 Ok(PersistentColony {
163 colony: Colony::new(),
164 persistence: None,
165 })
166 }
167}
168
169#[cfg(feature = "sqlite")]
171struct PersistenceState {
172 db: SqliteTopologyGraph,
173 path: PathBuf,
174 auto_save: bool,
175}
176
177#[cfg(not(feature = "sqlite"))]
178struct PersistenceState;
179
180pub struct PersistentColony {
185 colony: Colony,
186 #[cfg(feature = "sqlite")]
187 persistence: Option<PersistenceState>,
188 #[cfg(not(feature = "sqlite"))]
189 persistence: Option<PersistenceState>,
190}
191
192impl PersistentColony {
193 pub fn colony(&self) -> &Colony {
195 &self.colony
196 }
197
198 pub fn colony_mut(&mut self) -> &mut Colony {
200 &mut self.colony
201 }
202
203 pub fn into_inner(mut self) -> Colony {
207 #[cfg(feature = "sqlite")]
209 if let Some(ref mut state) = self.persistence {
210 state.auto_save = false;
211 }
212 let colony = std::mem::replace(&mut self.colony, Colony::new());
214 std::mem::forget(self); colony
216 }
217
218 pub fn has_persistence(&self) -> bool {
220 self.persistence.is_some()
221 }
222
223 #[cfg(feature = "sqlite")]
225 pub fn save(&mut self) -> Result<(), BuilderError> {
226 if let Some(ref mut state) = self.persistence {
227 save_to_sqlite(self.colony.substrate().graph(), &mut state.db)?;
228 }
229 Ok(())
230 }
231
232 #[cfg(not(feature = "sqlite"))]
233 pub fn save(&mut self) -> Result<(), BuilderError> {
234 Ok(())
235 }
236
237 #[cfg(feature = "sqlite")]
239 pub fn persistence_path(&self) -> Option<&Path> {
240 self.persistence.as_ref().map(|s| s.path.as_path())
241 }
242
243 #[cfg(not(feature = "sqlite"))]
244 pub fn persistence_path(&self) -> Option<&Path> {
245 None
246 }
247}
248
249impl PersistentColony {
251 pub fn run(&mut self, ticks: u64) -> Vec<Vec<crate::colony::ColonyEvent>> {
253 self.colony.run(ticks)
254 }
255
256 pub fn tick(&mut self) -> Vec<crate::colony::ColonyEvent> {
258 self.colony.tick()
259 }
260
261 pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
263 self.colony.ingest_document(title, content, position)
264 }
265
266 pub fn stats(&self) -> crate::colony::ColonyStats {
268 self.colony.stats()
269 }
270
271 pub fn snapshot(&self) -> crate::colony::ColonySnapshot {
273 self.colony.snapshot()
274 }
275
276 pub fn spawn(
278 &mut self,
279 agent: Box<dyn phago_core::agent::Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
280 ) -> AgentId {
281 self.colony.spawn(agent)
282 }
283
284 pub fn alive_count(&self) -> usize {
286 self.colony.alive_count()
287 }
288}
289
290#[cfg(feature = "sqlite")]
291impl Drop for PersistentColony {
292 fn drop(&mut self) {
293 if let Some(ref state) = self.persistence {
294 if state.auto_save {
295 let _ = save_to_sqlite(self.colony.substrate().graph(), &mut self.persistence.as_mut().unwrap().db);
297 }
298 }
299 }
300}
301
302#[cfg(feature = "sqlite")]
304fn load_from_sqlite(
305 source: &SqliteTopologyGraph,
306 target: &mut PetTopologyGraph,
307) -> Result<(), BuilderError> {
308 let mut node_count = 0;
310 for node in source.iter_nodes() {
311 target.add_node(node);
312 node_count += 1;
313 }
314
315 let mut edge_count = 0;
317 for (from, to, edge) in source.iter_edges() {
318 target.set_edge(from, to, edge);
319 edge_count += 1;
320 }
321
322 if node_count > 0 || edge_count > 0 {
323 eprintln!(
324 "Loaded {} nodes and {} edges from SQLite database.",
325 node_count, edge_count
326 );
327 }
328
329 Ok(())
330}
331
332#[cfg(feature = "sqlite")]
334fn save_to_sqlite(
335 source: &PetTopologyGraph,
336 target: &mut SqliteTopologyGraph,
337) -> Result<(), BuilderError> {
338 for node_id in source.all_nodes() {
340 if let Some(node) = source.get_node(&node_id) {
341 target.add_node(node.clone());
342 }
343 }
344
345 for (from, to, edge) in source.all_edges() {
347 target.set_edge(from, to, edge.clone());
348 }
349
350 Ok(())
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn build_simple_colony() {
359 let colony = ColonyBuilder::new().build_simple();
360 assert_eq!(colony.alive_count(), 0);
361 }
362
363 #[test]
364 fn build_without_persistence() {
365 let colony = ColonyBuilder::new().build().unwrap();
366 assert!(!colony.has_persistence());
367 }
368
369 #[cfg(feature = "sqlite")]
370 #[test]
371 fn build_with_persistence() {
372 let tmp = std::env::temp_dir().join("phago_builder_test.db");
373 let _ = std::fs::remove_file(&tmp); let mut colony = ColonyBuilder::new()
376 .with_persistence(&tmp)
377 .build()
378 .unwrap();
379
380 assert!(colony.has_persistence());
381 assert_eq!(colony.persistence_path(), Some(tmp.as_path()));
382
383 colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
385 colony.run(5);
386 colony.save().unwrap();
387
388 let _ = std::fs::remove_file(&tmp);
390 }
391
392 #[cfg(feature = "sqlite")]
393 #[test]
394 fn auto_save_on_drop() {
395 let tmp = std::env::temp_dir().join("phago_autosave_test.db");
396 let _ = std::fs::remove_file(&tmp);
397
398 {
399 let mut colony = ColonyBuilder::new()
400 .with_persistence(&tmp)
401 .auto_save(true)
402 .build()
403 .unwrap();
404
405 colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
406 colony.run(5);
407 }
409
410 assert!(tmp.exists());
412 let _ = std::fs::remove_file(&tmp);
413 }
414
415 #[cfg(feature = "sqlite")]
416 #[test]
417 fn roundtrip_save_load() {
418 use phago_agents::digester::Digester;
419
420 let tmp = std::env::temp_dir().join("phago_roundtrip_test.db");
421 let _ = std::fs::remove_file(&tmp);
422
423 let (node_count, edge_count) = {
425 let mut colony = ColonyBuilder::new()
426 .with_persistence(&tmp)
427 .build()
428 .unwrap();
429
430 colony.ingest_document("Biology 101", "Cell membrane proteins transport molecules", Position::new(0.0, 0.0));
431 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(50)));
432 colony.run(15);
433
434 let stats = colony.stats();
435 colony.save().unwrap();
436 (stats.graph_nodes, stats.graph_edges)
437 };
438
439 let colony2 = ColonyBuilder::new()
441 .with_persistence(&tmp)
442 .build()
443 .unwrap();
444
445 let stats2 = colony2.stats();
446
447 assert_eq!(stats2.graph_nodes, node_count, "Node count should match after reload");
449 assert_eq!(stats2.graph_edges, edge_count, "Edge count should match after reload");
450
451 let _ = std::fs::remove_file(&tmp);
452 }
453}