post_cortex_daemon/daemon/
config.rs1use serde::{Deserialize, Serialize};
27use std::fs;
28use std::path::PathBuf;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DaemonConfig {
33 pub host: String,
35
36 pub port: u16,
38
39 #[serde(default = "default_grpc_port")]
41 pub grpc_port: u16,
42
43 pub data_directory: String,
45
46 #[serde(default = "default_storage_backend")]
48 pub storage_backend: String,
49
50 #[serde(default)]
53 pub surrealdb_endpoint: Option<String>,
54
55 #[serde(default)]
57 pub surrealdb_username: Option<String>,
58
59 #[serde(default)]
61 pub surrealdb_password: Option<String>,
62
63 #[serde(default = "default_surrealdb_namespace")]
65 pub surrealdb_namespace: String,
66
67 #[serde(default = "default_surrealdb_database")]
69 pub surrealdb_database: String,
70}
71
72fn default_grpc_port() -> u16 {
73 3738
74}
75
76fn default_storage_backend() -> String {
77 "rocksdb".to_string()
78}
79
80fn default_surrealdb_namespace() -> String {
81 "post_cortex".to_string()
82}
83
84fn default_surrealdb_database() -> String {
85 "main".to_string()
86}
87
88impl Default for DaemonConfig {
89 fn default() -> Self {
90 Self {
91 host: "127.0.0.1".to_string(),
92 port: 3737,
93 grpc_port: default_grpc_port(),
94 data_directory: default_data_dir(),
95 storage_backend: default_storage_backend(),
96 surrealdb_endpoint: None,
97 surrealdb_username: None,
98 surrealdb_password: None,
99 surrealdb_namespace: default_surrealdb_namespace(),
100 surrealdb_database: default_surrealdb_database(),
101 }
102 }
103}
104
105impl DaemonConfig {
106 pub fn load() -> Self {
113 let config_path = default_config_path();
114
115 let mut config = Self::default();
117
118 if config_path.exists() {
120 match fs::read_to_string(&config_path) {
121 Ok(contents) => match toml::from_str::<DaemonConfig>(&contents) {
122 Ok(file_config) => {
123 config = file_config;
125 tracing::info!("Loaded configuration from {:?}", config_path);
126 }
127 Err(e) => {
128 tracing::warn!("Failed to parse config file {:?}: {}", config_path, e);
129 tracing::info!("Using default configuration");
130 }
131 },
132 Err(e) => {
133 tracing::warn!("Failed to read config file {:?}: {}", config_path, e);
134 tracing::info!("Using default configuration");
135 }
136 }
137 } else {
138 tracing::debug!("Config file {:?} not found, using defaults", config_path);
139 }
140
141 if let Ok(host) = std::env::var("PC_HOST") {
143 config.host = host;
144 tracing::debug!("Overriding host from PC_HOST environment variable");
145 }
146
147 if let Ok(port_str) = std::env::var("PC_PORT") {
148 if let Ok(port) = port_str.parse::<u16>() {
149 config.port = port;
150 tracing::debug!("Overriding port from PC_PORT environment variable");
151 } else {
152 tracing::warn!("Invalid PC_PORT value: {}", port_str);
153 }
154 }
155
156 if let Ok(grpc_port_str) = std::env::var("PC_GRPC_PORT")
157 && let Ok(port) = grpc_port_str.parse::<u16>()
158 {
159 config.grpc_port = port;
160 tracing::debug!("Overriding grpc_port from PC_GRPC_PORT environment variable");
161 }
162
163 if let Ok(data_dir) = std::env::var("PC_DATA_DIR") {
164 config.data_directory = data_dir;
165 tracing::debug!("Overriding data_directory from PC_DATA_DIR environment variable");
166 }
167
168 if let Ok(backend) = std::env::var("PC_STORAGE_BACKEND") {
170 config.storage_backend = backend;
171 tracing::debug!(
172 "Overriding storage_backend from PC_STORAGE_BACKEND environment variable"
173 );
174 }
175
176 if let Ok(endpoint) = std::env::var("PC_SURREALDB_ENDPOINT") {
177 config.surrealdb_endpoint = Some(endpoint);
178 tracing::debug!(
179 "Overriding surrealdb_endpoint from PC_SURREALDB_ENDPOINT environment variable"
180 );
181 }
182
183 if let Ok(username) = std::env::var("PC_SURREALDB_USER") {
184 config.surrealdb_username = Some(username);
185 tracing::debug!(
186 "Overriding surrealdb_username from PC_SURREALDB_USER environment variable"
187 );
188 }
189
190 if let Ok(password) = std::env::var("PC_SURREALDB_PASS") {
191 config.surrealdb_password = Some(password);
192 tracing::debug!(
193 "Overriding surrealdb_password from PC_SURREALDB_PASS environment variable"
194 );
195 }
196
197 config
198 }
199
200 pub fn create_example_config() -> Result<PathBuf, String> {
204 let config_path = default_config_path();
205
206 if config_path.exists() {
207 return Err(format!("Config file already exists at {:?}", config_path));
208 }
209
210 if let Some(parent) = config_path.parent() {
212 fs::create_dir_all(parent)
213 .map_err(|e| format!("Failed to create config directory: {}", e))?;
214 }
215
216 let example_config = DaemonConfig::default();
217 let toml_content = toml::to_string_pretty(&example_config)
218 .map_err(|e| format!("Failed to serialize config: {}", e))?;
219
220 let commented_toml = format!(
222 "# Post-Cortex Daemon Configuration\n\
223 # \n\
224 # This file configures the HTTP daemon server for multi-client access.\n\
225 # Environment variables override these settings:\n\
226 # PC_HOST - Override host\n\
227 # PC_PORT - Override port\n\
228 # PC_DATA_DIR - Override data directory\n\
229 # PC_STORAGE_BACKEND - Storage backend: \"rocksdb\" or \"surrealdb\"\n\
230 # PC_SURREALDB_ENDPOINT - SurrealDB WebSocket endpoint (e.g., \"ws://localhost:8000\")\n\
231 # PC_SURREALDB_USER - SurrealDB username\n\
232 # PC_SURREALDB_PASS - SurrealDB password\n\
233 # \n\
234 # Priority: Environment > Config file > Defaults\n\n\
235 {}",
236 toml_content
237 );
238
239 fs::write(&config_path, commented_toml)
240 .map_err(|e| format!("Failed to write config file: {}", e))?;
241
242 Ok(config_path)
243 }
244
245 pub fn validate(&self) -> Result<(), String> {
247 if self.host.is_empty() {
249 return Err("Host cannot be empty".to_string());
250 }
251
252 if self.port == 0 {
254 return Err("Port cannot be 0".to_string());
255 }
256
257 if self.data_directory.is_empty() {
259 return Err("Data directory cannot be empty".to_string());
260 }
261
262 Ok(())
263 }
264}
265
266fn default_config_path() -> PathBuf {
268 dirs::home_dir()
269 .unwrap_or_else(|| PathBuf::from("."))
270 .join(".post-cortex")
271 .join("daemon.toml")
272}
273
274fn default_data_dir() -> String {
276 dirs::home_dir()
277 .unwrap_or_else(|| PathBuf::from("."))
278 .join(".post-cortex/data")
279 .to_str()
280 .unwrap()
281 .to_string()
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use std::env;
288
289 #[test]
290 fn test_default_config() {
291 let config = DaemonConfig::default();
292 assert_eq!(config.host, "127.0.0.1");
293 assert_eq!(config.port, 3737);
294 assert!(config.data_directory.contains(".post-cortex/data"));
295 }
296
297 #[test]
298 fn test_config_validation() {
299 let config = DaemonConfig::default();
300 assert!(config.validate().is_ok());
301
302 let invalid = DaemonConfig {
303 host: "".to_string(),
304 ..Default::default()
305 };
306 assert!(invalid.validate().is_err());
307
308 let invalid_port = DaemonConfig {
309 port: 0,
310 ..Default::default()
311 };
312 assert!(invalid_port.validate().is_err());
313 }
314
315 #[test]
316 fn test_env_override() {
317 #[allow(unsafe_code)]
320 unsafe {
321 env::set_var("PC_HOST", "0.0.0.0");
322 env::set_var("PC_PORT", "8080");
323 env::set_var("PC_DATA_DIR", "/tmp/test-data");
324 }
325
326 let config = DaemonConfig::load();
327
328 assert_eq!(config.host, "0.0.0.0");
329 assert_eq!(config.port, 8080);
330 assert_eq!(config.data_directory, "/tmp/test-data");
331
332 #[allow(unsafe_code)]
334 unsafe {
335 env::remove_var("PC_HOST");
336 env::remove_var("PC_PORT");
337 env::remove_var("PC_DATA_DIR");
338 }
339 }
340
341 #[test]
342 fn test_toml_serialization() {
343 let config = DaemonConfig::default();
344 let toml_str = toml::to_string(&config).unwrap();
345 let parsed: DaemonConfig = toml::from_str(&toml_str).unwrap();
346
347 assert_eq!(config.host, parsed.host);
348 assert_eq!(config.port, parsed.port);
349 assert_eq!(config.data_directory, parsed.data_directory);
350 }
351}