use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const CONFIG_FILE_NAME: &str = "solidb-scripts.toml";
pub const ENV_API_KEY: &str = "SOLIDB_API_KEY";
pub const ENV_HOST: &str = "SOLIDB_HOST";
pub const ENV_PORT: &str = "SOLIDB_PORT";
pub const ENV_DATABASE: &str = "SOLIDB_DATABASE";
pub const ENV_SERVICE: &str = "SOLIDB_SERVICE";
pub const TEST_ENV_FILE: &str = ".env.test";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub host: String,
pub port: u16,
pub database: String,
#[serde(default)]
pub auth_token: String,
#[serde(default = "default_service")]
pub service: String,
#[serde(default)]
pub scripts: ScriptsConfig,
}
fn default_service() -> String {
"default".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptsConfig {
#[serde(default = "default_directory")]
pub directory: PathBuf,
#[serde(default = "default_tests_dir")]
pub tests_dir: String,
#[serde(default = "default_ignore")]
pub ignore: Vec<String>,
}
fn default_tests_dir() -> String {
"tests".to_string()
}
fn default_directory() -> PathBuf {
PathBuf::from(".")
}
fn default_ignore() -> Vec<String> {
vec![
"*.bak".to_string(),
".git".to_string(),
"node_modules".to_string(),
"tests".to_string(),
]
}
impl Default for ScriptsConfig {
fn default() -> Self {
Self {
directory: default_directory(),
tests_dir: default_tests_dir(),
ignore: default_ignore(),
}
}
}
impl Config {
pub fn new(host: String, port: u16, database: String) -> Self {
Self {
host,
port,
database,
auth_token: String::new(),
service: default_service(),
scripts: ScriptsConfig::default(),
}
}
pub fn load(dir: &Path) -> anyhow::Result<Self> {
Self::load_with_env(dir, ".env")
}
pub fn load_for_test(dir: &Path) -> anyhow::Result<Self> {
let test_env_path = dir.join(TEST_ENV_FILE);
if test_env_path.exists() {
return Self::load_with_env(dir, TEST_ENV_FILE);
}
Self::load_with_env(dir, ".env")
}
fn load_with_env(dir: &Path, env_file: &str) -> anyhow::Result<Self> {
let env_path = dir.join(env_file);
if env_path.exists() {
let _ = dotenvy::from_path(&env_path);
}
let config_path = dir.join(CONFIG_FILE_NAME);
if !config_path.exists() {
anyhow::bail!(
"Configuration file not found: {}\nRun 'solidb scripts init' to create one.",
config_path.display()
);
}
let content = std::fs::read_to_string(&config_path)?;
let mut config: Config = toml::from_str(&content)?;
config.apply_env_overrides();
Ok(config)
}
fn apply_env_overrides(&mut self) {
if let Ok(api_key) = std::env::var(ENV_API_KEY) {
if !api_key.is_empty() {
self.auth_token = api_key;
}
}
if let Ok(host) = std::env::var(ENV_HOST) {
if !host.is_empty() {
self.host = host;
}
}
if let Ok(port_str) = std::env::var(ENV_PORT) {
if let Ok(port) = port_str.parse::<u16>() {
self.port = port;
}
}
if let Ok(database) = std::env::var(ENV_DATABASE) {
if !database.is_empty() {
self.database = database;
}
}
if let Ok(service) = std::env::var(ENV_SERVICE) {
if !service.is_empty() {
self.service = service;
}
}
}
pub fn default_service(&self) -> String {
self.service.clone()
}
pub fn has_auth(&self) -> bool {
!self.auth_token.is_empty()
}
pub fn save(&self, dir: &Path) -> anyhow::Result<()> {
let config_path = dir.join(CONFIG_FILE_NAME);
let content = toml::to_string_pretty(self)?;
std::fs::write(&config_path, content)?;
Ok(())
}
pub fn base_url(&self) -> String {
format!("http://{}:{}", self.host, self.port)
}
pub fn scripts_dir(&self, config_dir: &Path) -> PathBuf {
if self.scripts.directory.is_absolute() {
self.scripts.directory.clone()
} else {
config_dir.join(&self.scripts.directory)
}
}
pub fn should_ignore(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.scripts.ignore {
if pattern.starts_with("*.") {
if let Some(ext) = pattern.strip_prefix("*.") {
if path_str.ends_with(&format!(".{}", ext)) {
return true;
}
}
} else if path_str.contains(pattern) {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_serialization() {
let config = Config::new("localhost".to_string(), 6745, "mydb".to_string());
let toml_str = toml::to_string_pretty(&config).unwrap();
assert!(toml_str.contains("host = \"localhost\""));
assert!(toml_str.contains("port = 6745"));
assert!(toml_str.contains("database = \"mydb\""));
}
#[test]
fn test_should_ignore() {
let config = Config::new("localhost".to_string(), 6745, "test".to_string());
assert!(config.should_ignore(Path::new("test.bak")));
assert!(config.should_ignore(Path::new(".git/config")));
assert!(config.should_ignore(Path::new("node_modules/package.json")));
assert!(!config.should_ignore(Path::new("users.lua")));
}
}