Skip to main content

plexus_substrate/activations/
storage.rs

1//! Common storage utilities for activation persistence
2//!
3//! This module provides shared infrastructure for SQLite-backed storage
4//! across different activations, including standardized path management
5//! and connection initialization.
6
7use sqlx::{sqlite::{SqliteConnectOptions, SqlitePool}, ConnectOptions};
8use std::path::PathBuf;
9
10/// Generate a namespaced database path under ~/.plexus/
11///
12/// Returns: `~/.plexus/substrate/activations/{activation_name}/{db_filename}`
13///
14/// # Arguments
15/// * `activation_name` - The name of the activation (e.g., "orcha", "claudecode")
16/// * `db_filename` - The database filename (e.g., "orcha.db", "sessions.db")
17///
18/// # Example
19/// ```ignore
20/// let path = activation_db_path("orcha", "orcha.db");
21/// // Returns: ~/.plexus/substrate/activations/orcha/orcha.db
22/// ```
23pub fn activation_db_path(activation_name: &str, db_filename: &str) -> PathBuf {
24    let home = std::env::var("HOME")
25        .or_else(|_| std::env::var("USERPROFILE"))
26        .unwrap_or_else(|_| ".".to_string());
27
28    PathBuf::from(home)
29        .join(".plexus")
30        .join("substrate")
31        .join("activations")
32        .join(activation_name)
33        .join(db_filename)
34}
35
36/// Extract activation name from module path
37///
38/// Extracts the activation name from a module path like:
39/// - `plexus_substrate::activations::orcha::storage` → `"orcha"`
40/// - `plexus_substrate::activations::claudecode_loopback::storage` → `"claudecode_loopback"`
41///
42/// # Arguments
43/// * `module_path` - The module path (typically from `module_path!()` macro)
44///
45/// # Example
46/// ```ignore
47/// // Called from src/activations/orcha/storage.rs
48/// let name = extract_activation_name(module_path!());
49/// assert_eq!(name, "orcha");
50/// ```
51pub fn extract_activation_name(module_path: &str) -> &str {
52    // Module path format: plexus_substrate::activations::{activation_name}::storage
53    // or: crate::activations::{activation_name}::storage
54    module_path
55        .split("::")
56        .skip_while(|&s| s != "activations")
57        .nth(1)
58        .unwrap_or("unknown")
59}
60
61/// Generate a namespaced database path from the calling module's path
62///
63/// This macro automatically extracts the activation name from the module structure
64/// and generates the appropriate database path.
65///
66/// # Example
67/// ```ignore
68/// // Called from src/activations/orcha/storage.rs
69/// let path = activation_db_path_from_module!("orcha.db");
70/// // Returns: ~/.plexus/substrate/activations/orcha/orcha.db
71/// ```
72#[macro_export]
73macro_rules! activation_db_path_from_module {
74    ($db_filename:expr) => {
75        $crate::activations::storage::activation_db_path(
76            $crate::activations::storage::extract_activation_name(module_path!()),
77            $db_filename
78        )
79    };
80}
81
82/// Initialize a SQLite connection pool with standard options
83///
84/// This helper:
85/// 1. Creates parent directories if they don't exist
86/// 2. Enables `create_if_missing` for the database
87/// 3. Disables statement logging
88/// 4. Returns a ready-to-use connection pool
89///
90/// # Arguments
91/// * `db_path` - Path to the SQLite database file
92///
93/// # Errors
94/// Returns an error if directory creation or database connection fails
95pub async fn init_sqlite_pool(db_path: PathBuf) -> Result<SqlitePool, String> {
96    // Ensure parent directory exists
97    if let Some(parent) = db_path.parent() {
98        std::fs::create_dir_all(parent)
99            .map_err(|e| format!("Failed to create database directory: {}", e))?;
100    }
101
102    // Parse connection options
103    let db_url = format!("sqlite://{}", db_path.display());
104    let options = db_url
105        .parse::<SqliteConnectOptions>()
106        .map_err(|e| format!("Failed to parse DB URL: {}", e))?;
107
108    // Configure SQLite options
109    let options = options
110        .disable_statement_logging()
111        .create_if_missing(true);
112
113    // Connect to database
114    SqlitePool::connect_with(options)
115        .await
116        .map_err(|e| format!("Failed to connect to database: {}", e))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_activation_db_path() {
125        let path = activation_db_path("orcha", "orcha.db");
126        let path_str = path.to_string_lossy();
127
128        assert!(path_str.contains(".plexus"));
129        assert!(path_str.contains("substrate"));
130        assert!(path_str.contains("activations"));
131        assert!(path_str.contains("orcha"));
132        assert!(path_str.ends_with("orcha.db"));
133    }
134
135    #[test]
136    fn test_activation_db_path_different_names() {
137        let path1 = activation_db_path("claudecode", "sessions.db");
138        let path2 = activation_db_path("cone", "cones.db");
139
140        assert!(path1.to_string_lossy().contains("claudecode/sessions.db"));
141        assert!(path2.to_string_lossy().contains("cone/cones.db"));
142        assert_ne!(path1, path2);
143    }
144
145    #[test]
146    fn test_extract_activation_name() {
147        assert_eq!(
148            extract_activation_name("plexus_substrate::activations::orcha::storage"),
149            "orcha"
150        );
151        assert_eq!(
152            extract_activation_name("plexus_substrate::activations::claudecode_loopback::storage"),
153            "claudecode_loopback"
154        );
155        assert_eq!(
156            extract_activation_name("crate::activations::cone::storage"),
157            "cone"
158        );
159    }
160
161    #[tokio::test]
162    async fn test_init_sqlite_pool() {
163        use std::time::{SystemTime, UNIX_EPOCH};
164
165        let timestamp = SystemTime::now()
166            .duration_since(UNIX_EPOCH)
167            .unwrap()
168            .as_nanos();
169
170        let test_db = PathBuf::from(format!("/tmp/test_storage_{}.db", timestamp));
171
172        let pool = init_sqlite_pool(test_db.clone()).await;
173        assert!(pool.is_ok(), "Failed to initialize SQLite pool");
174
175        // Cleanup
176        let _ = std::fs::remove_file(test_db);
177    }
178}