surrealdb_migrate_config/
lib.rs1mod env;
105
106use config::{Config, File, FileFormat};
107use database_migration::config::{DbAuthLevel, DbClientConfig, RunnerConfig};
108use database_migration::error::Error;
109use serde::de::{Unexpected, Visitor};
110use serde::{Deserialize, Deserializer};
111use std::collections::HashMap;
112use std::fmt::{Formatter, Write as _};
113use std::path::Path;
114
115pub const CONFIG_DIR_ENVIRONMENT_VAR: &str = "SURREALDB_MIGRATE_CONFIG_DIR";
116pub const CONFIG_FILENAME: &str = "surrealdb-migrate";
117
118const DEFAULT_SETTINGS: &str = include_str!("../resources/surrealdb-migrate.default.toml");
119
120#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
121pub struct Settings {
122 pub migration: MigrationSettings,
123 pub files: FilesSettings,
124 pub database: DatabaseSettings,
125}
126
127#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
128#[serde(rename_all = "kebab-case")]
129pub struct MigrationSettings {
130 pub ignore_checksum: bool,
131 pub ignore_order: bool,
132}
133
134#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
135#[serde(rename_all = "kebab-case")]
136pub struct FilesSettings {
137 pub migrations_folder: String,
138 pub script_extension: String,
139 pub up_script_extension: String,
140 pub down_script_extension: String,
141 pub exclude: String,
142}
143
144#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
145#[serde(rename_all = "kebab-case")]
146pub struct DatabaseSettings {
147 pub migrations_table: String,
148 pub address: String,
149 pub username: String,
150 pub password: String,
151 #[serde(deserialize_with = "db_auth_level_from_string")]
152 pub auth_level: DbAuthLevel,
153 pub namespace: String,
154 pub database: String,
155 pub capacity: usize,
156}
157
158fn read_environment() -> String {
159 const MIGRATION_PREFIX: &str = "SURMIG_MIGRATION_";
160 const FILES_PREFIX: &str = "SURMIG_FILES_";
161 const DATABASE_PREFIX: &str = "SURMIG_DATABASE_";
162
163 let mut migration = HashMap::new();
164 let mut files = HashMap::new();
165 let mut database = HashMap::new();
166
167 for (key, val) in env::vars() {
168 if key.starts_with(MIGRATION_PREFIX) {
169 let offset = MIGRATION_PREFIX.len();
170 migration.insert(to_kebab_case(&key, offset), val);
171 } else if key.starts_with(DATABASE_PREFIX) {
172 let offset = DATABASE_PREFIX.len();
173 database.insert(to_kebab_case(&key, offset), val);
174 } else if key.starts_with(FILES_PREFIX) {
175 let offset = FILES_PREFIX.len();
176 files.insert(to_kebab_case(&key, offset), val);
177 }
178 }
179
180 let mut environment_toml = String::new();
181 if !migration.is_empty() {
182 environment_toml.push_str("[migration]\n");
183 for (key, val) in migration {
184 let _ = writeln!(environment_toml, "{key} = \"{val}\"");
185 }
186 }
187 if !files.is_empty() {
188 environment_toml.push_str("[files]\n");
189 for (key, val) in files {
190 let _ = writeln!(environment_toml, "{key} = \"{val}\"");
191 }
192 }
193 if !database.is_empty() {
194 environment_toml.push_str("[database]\n");
195 for (key, val) in database {
196 let _ = writeln!(environment_toml, "{key} = \"{val}\"");
197 }
198 }
199 environment_toml
200}
201
202fn to_kebab_case(s: &str, offset: usize) -> String {
203 s.chars()
204 .skip(offset)
205 .map(|c| {
206 if c == '_' {
207 '-'
208 } else {
209 c.to_ascii_lowercase()
210 }
211 })
212 .collect()
213}
214
215fn db_auth_level_from_string<'de, D>(deserializer: D) -> Result<DbAuthLevel, D::Error>
216where
217 D: Deserializer<'de>,
218{
219 deserializer.deserialize_str(DbAuthLevelVisitor)
220}
221
222struct DbAuthLevelVisitor;
223
224impl Visitor<'_> for DbAuthLevelVisitor {
225 type Value = DbAuthLevel;
226
227 fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
228 formatter
229 .write_str("expecting a string containing one of 'Root', 'Namespace' or 'Database'")
230 }
231
232 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
233 where
234 E: serde::de::Error,
235 {
236 match &v.to_ascii_lowercase()[..] {
237 "root" => Ok(DbAuthLevel::Root),
238 "namespace" => Ok(DbAuthLevel::Namespace),
239 "database" => Ok(DbAuthLevel::Database),
240 _ => Err(serde::de::Error::invalid_value(
241 Unexpected::Str(v),
242 &"Root, Namespace or Database",
243 )),
244 }
245 }
246}
247
248impl Settings {
249 pub fn load() -> Result<Self, Error> {
250 let config_dir = env::var(CONFIG_DIR_ENVIRONMENT_VAR).unwrap_or_else(|_| "./".into());
251 Self::load_from_dir(Path::new(&config_dir))
252 }
253
254 pub fn load_from_dir(path: &Path) -> Result<Self, Error> {
255 let environment = read_environment();
256 let config_file = path.join(CONFIG_FILENAME);
257 let config = Config::builder()
258 .add_source(File::from_str(DEFAULT_SETTINGS, FileFormat::Toml))
259 .add_source(File::from(config_file).required(false))
260 .add_source(File::from_str(&environment, FileFormat::Toml))
261 .build()
262 .map_err(|err| Error::Configuration(err.to_string()))?;
263
264 config
265 .try_deserialize()
266 .map_err(|err| Error::Configuration(err.to_string()))
267 }
268
269 pub fn runner_config(&self) -> RunnerConfig<'_> {
270 RunnerConfig {
271 migrations_folder: Path::new(&self.files.migrations_folder).into(),
272 excluded_files: self.files.exclude.parse().unwrap_or_else(|err| panic!("failed to create default `RunnerConfig`: {err} -- THIS IS AN IMPLEMENTATION ERROR! Please file a bug.")),
273 migrations_table: (&self.database.migrations_table).into(),
274 ignore_checksum: self.migration.ignore_checksum,
275 ignore_order: self.migration.ignore_order,
276 }
277 }
278
279 pub fn db_client_config(&self) -> DbClientConfig<'_> {
280 DbClientConfig {
281 address: (&self.database.address).into(),
282 namespace: (&self.database.namespace).into(),
283 database: (&self.database.database).into(),
284 auth_level: self.database.auth_level,
285 username: (&self.database.username).into(),
286 password: (&self.database.password).into(),
287 capacity: self.database.capacity,
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests;