use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonConfig {
pub host: String,
pub port: u16,
#[serde(default = "default_grpc_port")]
pub grpc_port: u16,
pub data_directory: String,
#[serde(default = "default_storage_backend")]
pub storage_backend: String,
#[serde(default)]
pub surrealdb_endpoint: Option<String>,
#[serde(default)]
pub surrealdb_username: Option<String>,
#[serde(default)]
pub surrealdb_password: Option<String>,
#[serde(default = "default_surrealdb_namespace")]
pub surrealdb_namespace: String,
#[serde(default = "default_surrealdb_database")]
pub surrealdb_database: String,
}
fn default_grpc_port() -> u16 {
3738
}
fn default_storage_backend() -> String {
"rocksdb".to_string()
}
fn default_surrealdb_namespace() -> String {
"post_cortex".to_string()
}
fn default_surrealdb_database() -> String {
"main".to_string()
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3737,
grpc_port: default_grpc_port(),
data_directory: default_data_dir(),
storage_backend: default_storage_backend(),
surrealdb_endpoint: None,
surrealdb_username: None,
surrealdb_password: None,
surrealdb_namespace: default_surrealdb_namespace(),
surrealdb_database: default_surrealdb_database(),
}
}
}
impl DaemonConfig {
pub fn load() -> Self {
let config_path = default_config_path();
let mut config = Self::default();
if config_path.exists() {
match fs::read_to_string(&config_path) {
Ok(contents) => match toml::from_str::<DaemonConfig>(&contents) {
Ok(file_config) => {
config = file_config;
tracing::info!("Loaded configuration from {:?}", config_path);
}
Err(e) => {
tracing::warn!("Failed to parse config file {:?}: {}", config_path, e);
tracing::info!("Using default configuration");
}
},
Err(e) => {
tracing::warn!("Failed to read config file {:?}: {}", config_path, e);
tracing::info!("Using default configuration");
}
}
} else {
tracing::debug!("Config file {:?} not found, using defaults", config_path);
}
if let Ok(host) = std::env::var("PC_HOST") {
config.host = host;
tracing::debug!("Overriding host from PC_HOST environment variable");
}
if let Ok(port_str) = std::env::var("PC_PORT") {
if let Ok(port) = port_str.parse::<u16>() {
config.port = port;
tracing::debug!("Overriding port from PC_PORT environment variable");
} else {
tracing::warn!("Invalid PC_PORT value: {}", port_str);
}
}
if let Ok(grpc_port_str) = std::env::var("PC_GRPC_PORT")
&& let Ok(port) = grpc_port_str.parse::<u16>()
{
config.grpc_port = port;
tracing::debug!("Overriding grpc_port from PC_GRPC_PORT environment variable");
}
if let Ok(data_dir) = std::env::var("PC_DATA_DIR") {
config.data_directory = data_dir;
tracing::debug!("Overriding data_directory from PC_DATA_DIR environment variable");
}
if let Ok(backend) = std::env::var("PC_STORAGE_BACKEND") {
config.storage_backend = backend;
tracing::debug!(
"Overriding storage_backend from PC_STORAGE_BACKEND environment variable"
);
}
if let Ok(endpoint) = std::env::var("PC_SURREALDB_ENDPOINT") {
config.surrealdb_endpoint = Some(endpoint);
tracing::debug!(
"Overriding surrealdb_endpoint from PC_SURREALDB_ENDPOINT environment variable"
);
}
if let Ok(username) = std::env::var("PC_SURREALDB_USER") {
config.surrealdb_username = Some(username);
tracing::debug!(
"Overriding surrealdb_username from PC_SURREALDB_USER environment variable"
);
}
if let Ok(password) = std::env::var("PC_SURREALDB_PASS") {
config.surrealdb_password = Some(password);
tracing::debug!(
"Overriding surrealdb_password from PC_SURREALDB_PASS environment variable"
);
}
config
}
pub fn create_example_config() -> Result<PathBuf, String> {
let config_path = default_config_path();
if config_path.exists() {
return Err(format!("Config file already exists at {:?}", config_path));
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
let example_config = DaemonConfig::default();
let toml_content = toml::to_string_pretty(&example_config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
let commented_toml = format!(
"# Post-Cortex Daemon Configuration\n\
# \n\
# This file configures the HTTP daemon server for multi-client access.\n\
# Environment variables override these settings:\n\
# PC_HOST - Override host\n\
# PC_PORT - Override port\n\
# PC_DATA_DIR - Override data directory\n\
# PC_STORAGE_BACKEND - Storage backend: \"rocksdb\" or \"surrealdb\"\n\
# PC_SURREALDB_ENDPOINT - SurrealDB WebSocket endpoint (e.g., \"ws://localhost:8000\")\n\
# PC_SURREALDB_USER - SurrealDB username\n\
# PC_SURREALDB_PASS - SurrealDB password\n\
# \n\
# Priority: Environment > Config file > Defaults\n\n\
{}",
toml_content
);
fs::write(&config_path, commented_toml)
.map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(config_path)
}
pub fn validate(&self) -> Result<(), String> {
if self.host.is_empty() {
return Err("Host cannot be empty".to_string());
}
if self.port == 0 {
return Err("Port cannot be 0".to_string());
}
if self.data_directory.is_empty() {
return Err("Data directory cannot be empty".to_string());
}
Ok(())
}
}
fn default_config_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".post-cortex")
.join("daemon.toml")
}
fn default_data_dir() -> String {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".post-cortex/data")
.to_str()
.unwrap()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_default_config() {
let config = DaemonConfig::default();
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 3737);
assert!(config.data_directory.contains(".post-cortex/data"));
}
#[test]
fn test_config_validation() {
let config = DaemonConfig::default();
assert!(config.validate().is_ok());
let invalid = DaemonConfig {
host: "".to_string(),
..Default::default()
};
assert!(invalid.validate().is_err());
let invalid_port = DaemonConfig {
port: 0,
..Default::default()
};
assert!(invalid_port.validate().is_err());
}
#[test]
fn test_env_override() {
#[allow(unsafe_code)]
unsafe {
env::set_var("PC_HOST", "0.0.0.0");
env::set_var("PC_PORT", "8080");
env::set_var("PC_DATA_DIR", "/tmp/test-data");
}
let config = DaemonConfig::load();
assert_eq!(config.host, "0.0.0.0");
assert_eq!(config.port, 8080);
assert_eq!(config.data_directory, "/tmp/test-data");
#[allow(unsafe_code)]
unsafe {
env::remove_var("PC_HOST");
env::remove_var("PC_PORT");
env::remove_var("PC_DATA_DIR");
}
}
#[test]
fn test_toml_serialization() {
let config = DaemonConfig::default();
let toml_str = toml::to_string(&config).unwrap();
let parsed: DaemonConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config.host, parsed.host);
assert_eq!(config.port, parsed.port);
assert_eq!(config.data_directory, parsed.data_directory);
}
}