1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5
6#[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
48fn 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 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 config.override_from_env();
115
116 config.validate()?;
118
119 Ok(config)
120 }
121
122 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 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 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 pub fn connection_uri(&self) -> String {
195 format!(
196 "{}:{}",
197 self.database.hosts.join(","),
198 self.database.port
199 )
200 }
201
202 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}