db_migrate/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5
6/// Main configuration structure
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    pub database: DatabaseConfig,
10    pub migrations: MigrationsConfig,
11    pub behavior: BehaviorConfig,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DatabaseConfig {
16    pub hosts: Vec<String>,
17    pub keyspace: String,
18    #[serde(default)]
19    pub username: String,
20    #[serde(default)]
21    pub password: String,
22    #[serde(default = "default_port")]
23    pub port: u16,
24    #[serde(default = "default_datacenter")]
25    pub datacenter: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct MigrationsConfig {
30    #[serde(default = "default_migrations_dir")]
31    pub directory: PathBuf,
32    #[serde(default = "default_table_name")]
33    pub table_name: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct BehaviorConfig {
38    #[serde(default = "default_true")]
39    pub auto_create_keyspace: bool,
40    #[serde(default = "default_true")]
41    pub verify_checksums: bool,
42    #[serde(default = "default_false")]
43    pub allow_destructive: bool,
44    #[serde(default = "default_timeout")]
45    pub timeout_seconds: u64,
46}
47
48// Default value functions
49fn default_port() -> u16 {
50    9042
51}
52
53fn default_datacenter() -> String {
54    "datacenter1".to_string()
55}
56
57fn default_migrations_dir() -> PathBuf {
58    PathBuf::from("./migrations")
59}
60
61fn default_table_name() -> String {
62    "schema_migrations".to_string()
63}
64
65fn default_true() -> bool {
66    true
67}
68
69fn default_false() -> bool {
70    false
71}
72
73fn default_timeout() -> u64 {
74    30
75}
76
77impl Default for Config {
78    fn default() -> Self {
79        Self {
80            database: DatabaseConfig {
81                hosts: vec!["127.0.0.1".to_string()],
82                keyspace: "migrations_test".to_string(),
83                username: String::new(),
84                password: String::new(),
85                port: default_port(),
86                datacenter: default_datacenter(),
87            },
88            migrations: MigrationsConfig {
89                directory: default_migrations_dir(),
90                table_name: default_table_name(),
91            },
92            behavior: BehaviorConfig {
93                auto_create_keyspace: default_true(),
94                verify_checksums: default_true(),
95                allow_destructive: default_false(),
96                timeout_seconds: default_timeout(),
97            },
98        }
99    }
100}
101
102impl Config {
103    /// Load configuration from file and environment variables
104    pub async fn load<P: AsRef<Path>>(config_path: P) -> Result<Self> {
105        let mut config = if config_path.as_ref().exists() {
106            let content = fs::read_to_string(config_path).await?;
107            toml::from_str::<Config>(&content)?
108        } else {
109            tracing::info!("Config file not found, using defaults");
110            Config::default()
111        };
112
113        // Override with environment variables if present
114        config.override_from_env();
115
116        // Validate configuration
117        config.validate()?;
118
119        Ok(config)
120    }
121
122    /// Override configuration values from environment variables
123    fn override_from_env(&mut self) {
124        if let Ok(hosts) = std::env::var("DB_MIGRATE_HOSTS") {
125            self.database.hosts = hosts
126                .split(',')
127                .map(|s| s.trim().to_string())
128                .collect();
129        }
130
131        if let Ok(keyspace) = std::env::var("DB_MIGRATE_KEYSPACE") {
132            self.database.keyspace = keyspace;
133        }
134
135        if let Ok(username) = std::env::var("DB_MIGRATE_USERNAME") {
136            self.database.username = username;
137        }
138
139        if let Ok(password) = std::env::var("DB_MIGRATE_PASSWORD") {
140            self.database.password = password;
141        }
142
143        if let Ok(migrations_dir) = std::env::var("DB_MIGRATE_MIGRATIONS_DIR") {
144            self.migrations.directory = PathBuf::from(migrations_dir);
145        }
146
147        if let Ok(table_name) = std::env::var("DB_MIGRATE_TABLE_NAME") {
148            self.migrations.table_name = table_name;
149        }
150
151        if let Ok(auto_create) = std::env::var("DB_MIGRATE_AUTO_CREATE_KEYSPACE") {
152            self.behavior.auto_create_keyspace = auto_create.parse().unwrap_or(true);
153        }
154
155        if let Ok(verify_checksums) = std::env::var("DB_MIGRATE_VERIFY_CHECKSUMS") {
156            self.behavior.verify_checksums = verify_checksums.parse().unwrap_or(true);
157        }
158
159        if let Ok(allow_destructive) = std::env::var("DB_MIGRATE_ALLOW_DESTRUCTIVE") {
160            self.behavior.allow_destructive = allow_destructive.parse().unwrap_or(false);
161        }
162    }
163
164    /// Validate configuration values
165    fn validate(&self) -> Result<()> {
166        if self.database.hosts.is_empty() {
167            anyhow::bail!("At least one database host must be specified");
168        }
169
170        if self.database.keyspace.is_empty() {
171            anyhow::bail!("Database keyspace must be specified");
172        }
173
174        if self.migrations.table_name.is_empty() {
175            anyhow::bail!("Migrations table name cannot be empty");
176        }
177
178        // Validate that migrations directory exists or can be created
179        if !self.migrations.directory.exists() {
180            if let Some(parent) = self.migrations.directory.parent() {
181                if !parent.exists() {
182                    anyhow::bail!(
183                        "Migrations directory parent '{}' does not exist",
184                        parent.display()
185                    );
186                }
187            }
188        }
189
190        Ok(())
191    }
192
193    /// Get the full connection string for ScyllaDB
194    pub fn connection_uri(&self) -> String {
195        format!(
196            "{}:{}",
197            self.database.hosts.join(","),
198            self.database.port
199        )
200    }
201
202    /// Create a default configuration file
203    pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
204        let config = Config::default();
205        let content = toml::to_string_pretty(&config)?;
206        fs::write(path, content).await?;
207        Ok(())
208    }
209}