1use crate::colony::{Colony, ColonyConfig};
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 colony_config: ColonyConfig,
74}
75
76impl Default for ColonyBuilder {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl ColonyBuilder {
83 pub fn new() -> Self {
85 Self {
86 persistence_path: None,
87 auto_save: false,
88 cache_size: 1000,
89 colony_config: ColonyConfig::default(),
90 }
91 }
92
93 pub fn with_config(mut self, config: ColonyConfig) -> Self {
98 self.colony_config = config;
99 self
100 }
101
102 #[cfg(feature = "sqlite")]
107 pub fn with_persistence<P: AsRef<Path>>(mut self, path: P) -> Self {
108 self.persistence_path = Some(path.as_ref().to_path_buf());
109 self
110 }
111
112 #[cfg(not(feature = "sqlite"))]
114 pub fn with_persistence<P: AsRef<Path>>(self, _path: P) -> Self {
115 self
117 }
118
119 pub fn auto_save(mut self, enabled: bool) -> Self {
124 self.auto_save = enabled;
125 self
126 }
127
128 pub fn cache_size(mut self, size: usize) -> Self {
130 self.cache_size = size;
131 self
132 }
133
134 pub fn build_simple(self) -> Colony {
136 Colony::from_config(self.colony_config)
137 }
138
139 #[cfg(feature = "sqlite")]
141 pub fn build(self) -> Result<PersistentColony, BuilderError> {
142 let mut colony = Colony::from_config(self.colony_config);
143
144 let persistence = if let Some(path) = self.persistence_path {
145 let db = SqliteTopologyGraph::open(&path)
146 .map_err(|e| BuilderError::DatabaseError(e.to_string()))?
147 .with_cache_size(self.cache_size);
148
149 load_from_sqlite(&db, colony.substrate_mut().graph_mut())?;
151
152 Some(PersistenceState {
153 db,
154 path,
155 auto_save: self.auto_save,
156 })
157 } else {
158 None
159 };
160
161 Ok(PersistentColony {
162 colony,
163 persistence,
164 })
165 }
166
167 #[cfg(not(feature = "sqlite"))]
169 pub fn build(self) -> Result<PersistentColony, BuilderError> {
170 if self.persistence_path.is_some() {
171 return Err(BuilderError::SqliteNotEnabled);
172 }
173 Ok(PersistentColony {
174 colony: Colony::from_config(self.colony_config),
175 persistence: None,
176 })
177 }
178}
179
180#[cfg(feature = "sqlite")]
182struct PersistenceState {
183 db: SqliteTopologyGraph,
184 path: PathBuf,
185 auto_save: bool,
186}
187
188#[cfg(not(feature = "sqlite"))]
189struct PersistenceState;
190
191pub struct PersistentColony {
196 colony: Colony,
197 #[cfg(feature = "sqlite")]
198 persistence: Option<PersistenceState>,
199 #[cfg(not(feature = "sqlite"))]
200 persistence: Option<PersistenceState>,
201}
202
203impl PersistentColony {
204 pub fn colony(&self) -> &Colony {
206 &self.colony
207 }
208
209 pub fn colony_mut(&mut self) -> &mut Colony {
211 &mut self.colony
212 }
213
214 pub fn into_inner(mut self) -> Colony {
218 #[cfg(feature = "sqlite")]
220 if let Some(ref mut state) = self.persistence {
221 state.auto_save = false;
222 }
223 let colony = std::mem::replace(&mut self.colony, Colony::new());
225 std::mem::forget(self); colony
227 }
228
229 pub fn has_persistence(&self) -> bool {
231 self.persistence.is_some()
232 }
233
234 #[cfg(feature = "sqlite")]
236 pub fn save(&mut self) -> Result<(), BuilderError> {
237 if let Some(ref mut state) = self.persistence {
238 save_to_sqlite(self.colony.substrate().graph(), &mut state.db)?;
239 }
240 Ok(())
241 }
242
243 #[cfg(not(feature = "sqlite"))]
244 pub fn save(&mut self) -> Result<(), BuilderError> {
245 Ok(())
246 }
247
248 #[cfg(feature = "sqlite")]
250 pub fn persistence_path(&self) -> Option<&Path> {
251 self.persistence.as_ref().map(|s| s.path.as_path())
252 }
253
254 #[cfg(not(feature = "sqlite"))]
255 pub fn persistence_path(&self) -> Option<&Path> {
256 None
257 }
258}
259
260impl PersistentColony {
262 pub fn run(&mut self, ticks: u64) -> Vec<Vec<crate::colony::ColonyEvent>> {
264 self.colony.run(ticks)
265 }
266
267 pub fn tick(&mut self) -> Vec<crate::colony::ColonyEvent> {
269 self.colony.tick()
270 }
271
272 pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
274 self.colony.ingest_document(title, content, position)
275 }
276
277 pub fn stats(&self) -> crate::colony::ColonyStats {
279 self.colony.stats()
280 }
281
282 pub fn snapshot(&self) -> crate::colony::ColonySnapshot {
284 self.colony.snapshot()
285 }
286
287 pub fn spawn(
289 &mut self,
290 agent: Box<dyn phago_core::agent::Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
291 ) -> AgentId {
292 self.colony.spawn(agent)
293 }
294
295 pub fn alive_count(&self) -> usize {
297 self.colony.alive_count()
298 }
299}
300
301#[cfg(feature = "sqlite")]
302impl Drop for PersistentColony {
303 fn drop(&mut self) {
304 if let Some(ref state) = self.persistence {
305 if state.auto_save {
306 let _ = save_to_sqlite(self.colony.substrate().graph(), &mut self.persistence.as_mut().unwrap().db);
308 }
309 }
310 }
311}
312
313#[cfg(feature = "sqlite")]
315fn load_from_sqlite(
316 source: &SqliteTopologyGraph,
317 target: &mut PetTopologyGraph,
318) -> Result<(), BuilderError> {
319 let mut node_count = 0;
321 for node in source.iter_nodes() {
322 target.add_node(node);
323 node_count += 1;
324 }
325
326 let mut edge_count = 0;
328 for (from, to, edge) in source.iter_edges() {
329 target.set_edge(from, to, edge);
330 edge_count += 1;
331 }
332
333 if node_count > 0 || edge_count > 0 {
334 eprintln!(
335 "Loaded {} nodes and {} edges from SQLite database.",
336 node_count, edge_count
337 );
338 }
339
340 Ok(())
341}
342
343#[cfg(feature = "sqlite")]
345fn save_to_sqlite(
346 source: &PetTopologyGraph,
347 target: &mut SqliteTopologyGraph,
348) -> Result<(), BuilderError> {
349 for node_id in source.all_nodes() {
351 if let Some(node) = source.get_node(&node_id) {
352 target.add_node(node.clone());
353 }
354 }
355
356 for (from, to, edge) in source.all_edges() {
358 target.set_edge(from, to, edge.clone());
359 }
360
361 Ok(())
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn build_simple_colony() {
370 let colony = ColonyBuilder::new().build_simple();
371 assert_eq!(colony.alive_count(), 0);
372 }
373
374 #[test]
375 fn build_without_persistence() {
376 let colony = ColonyBuilder::new().build().unwrap();
377 assert!(!colony.has_persistence());
378 }
379
380 #[cfg(feature = "sqlite")]
381 #[test]
382 fn build_with_persistence() {
383 let tmp = std::env::temp_dir().join("phago_builder_test.db");
384 let _ = std::fs::remove_file(&tmp); let mut colony = ColonyBuilder::new()
387 .with_persistence(&tmp)
388 .build()
389 .unwrap();
390
391 assert!(colony.has_persistence());
392 assert_eq!(colony.persistence_path(), Some(tmp.as_path()));
393
394 colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
396 colony.run(5);
397 colony.save().unwrap();
398
399 let _ = std::fs::remove_file(&tmp);
401 }
402
403 #[cfg(feature = "sqlite")]
404 #[test]
405 fn auto_save_on_drop() {
406 let tmp = std::env::temp_dir().join("phago_autosave_test.db");
407 let _ = std::fs::remove_file(&tmp);
408
409 {
410 let mut colony = ColonyBuilder::new()
411 .with_persistence(&tmp)
412 .auto_save(true)
413 .build()
414 .unwrap();
415
416 colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
417 colony.run(5);
418 }
420
421 assert!(tmp.exists());
423 let _ = std::fs::remove_file(&tmp);
424 }
425
426 #[cfg(feature = "sqlite")]
427 #[test]
428 fn roundtrip_save_load() {
429 use phago_agents::digester::Digester;
430
431 let tmp = std::env::temp_dir().join("phago_roundtrip_test.db");
432 let _ = std::fs::remove_file(&tmp);
433
434 let (node_count, edge_count) = {
436 let mut colony = ColonyBuilder::new()
437 .with_persistence(&tmp)
438 .build()
439 .unwrap();
440
441 colony.ingest_document("Biology 101", "Cell membrane proteins transport molecules", Position::new(0.0, 0.0));
442 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(50)));
443 colony.run(15);
444
445 let stats = colony.stats();
446 colony.save().unwrap();
447 (stats.graph_nodes, stats.graph_edges)
448 };
449
450 let colony2 = ColonyBuilder::new()
452 .with_persistence(&tmp)
453 .build()
454 .unwrap();
455
456 let stats2 = colony2.stats();
457
458 assert_eq!(stats2.graph_nodes, node_count, "Node count should match after reload");
460 assert_eq!(stats2.graph_edges, edge_count, "Edge count should match after reload");
461
462 let _ = std::fs::remove_file(&tmp);
463 }
464}