1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use std::{path::PathBuf, env::current_dir, fs::read_to_string};
use anyhow::anyhow;
use toml_edit::{Document, ArrayOfTables, value};
use crate::{backend::{DatabaseBackend, pg::PgBackend}};
pub fn config_file_exists_in_dir(dir: &mut PathBuf) -> bool {
dir.push("Salmo.toml");
let exists = dir.exists();
dir.pop();
exists
}
pub fn find_config_file() -> Option<PathBuf> {
let mut dir = current_dir().ok()?;
if config_file_exists_in_dir(&mut dir) {
return Some(dir)
}
while dir.pop() {
if config_file_exists_in_dir(&mut dir) {
return Some(dir)
}
}
None
}
fn parse_envs(toml: &ArrayOfTables) -> anyhow::Result<Vec<ConfigEnvironment>> {
toml.iter().map(|t| {
let name = t.get("name").and_then(|p| p.as_str()).ok_or_else(|| anyhow!("missing 'name' in 'environments'"))?.to_owned();
let is_production = t.get("is_production").unwrap_or(&value(false)).as_bool().unwrap_or(false);
let connection = parse_connection(t)?;
let schema_name = t.get("metadata_schema_name").and_then(|s| s.as_str().to_owned()).map(|s| s.to_owned());
Ok(ConfigEnvironment {
name, is_production, connection, schema_name
})
}).collect()
}
fn string_or_env(opt: Option<&toml_edit::Item>) -> anyhow::Result<Option<String>> {
if let Some(v) = opt {
if let Some(table) = v.as_inline_table() {
let env = table.get("env").and_then(|e| e.as_str()).ok_or_else(|| anyhow!("a string or env key was specified as a table, but did not have a key 'env'"))?;
let env_val = std::env::var(env).ok();
Ok(env_val)
} else if let Some(str) = v.as_str() {
Ok(Some(str.to_owned()))
} else {
Err(anyhow!("string or env value was not a string or env"))
}
} else {
Ok(None)
}
}
fn parse_connection(t: &toml_edit::Table) -> anyhow::Result<ConnectionInfo> {
if t.contains_key("connection_string") {
return Ok(ConnectionInfo::Url(string_or_env(t.get("connection_string"))?))
}
if t.contains_key("connection_params") {
let params = t.get("connection_params").and_then(|c| c.as_table_like()).ok_or_else(|| anyhow!("connection_params provided, but no parameters were provided"))?;
let user = Ok(params.get("user")).and_then(string_or_env)?;
let password = Ok(params.get("password")).and_then(string_or_env)?;
let dbname = Ok(params.get("dbname")).and_then(string_or_env)?;
let options = Ok(params.get("options")).and_then(string_or_env)?;
let host = Ok(params.get("host")).and_then(string_or_env)?;
let port = Ok(params.get("port")).and_then(string_or_env)?;
return Ok(ConnectionInfo::Params { user, password, dbname, options, host, port })
}
Err(anyhow!("'environment' must contain a connection -- either 'connection_string' or 'connection_params'"))
}
pub fn get_config() -> anyhow::Result<Config>{
let dir = find_config_file().ok_or_else(|| anyhow!("No Salmo.toml file found"))?;
let config_str = read_to_string(dir.join("Salmo.toml"))?;
let doc = config_str.parse::<Document>()?;
let mdir = doc.get("migrations_directory").and_then(|d| d.as_str()).ok_or_else(|| anyhow!("Missing key 'migrations_directory' in Salmo.toml"))?;
let environments = parse_envs(doc.get("environments").and_then(|e| e.as_array_of_tables()).ok_or_else(|| anyhow!("Missing key 'environments' in Salmo.toml"))?)?;
let default_environments = doc.get("default_environments").and_then(|de| de.as_array())
.ok_or_else(|| anyhow!("Missing key 'default_environments' in Salmo.toml"))?.iter()
.map(|e|
e.as_str()
.ok_or_else(|| anyhow!("values in 'default_environments' must be strings"))
.map(|s| s.to_owned())
).collect::<anyhow::Result<Vec<_>>>()?;
let migrations_directory = dir.join(mdir);
if !migrations_directory.exists() {
return Err(anyhow!("migrations_directory does not exist"))
}
Ok(Config {
migrations_directory,
environments,
default_environments
})
}
#[derive(Debug, Clone)]
pub struct Config {
pub migrations_directory: PathBuf,
pub environments: Vec<ConfigEnvironment>,
pub default_environments: Vec<String>
}
#[derive(Debug, Clone)]
pub struct ConfigEnvironment {
pub name: String,
pub is_production: bool,
pub connection: ConnectionInfo,
pub schema_name: Option<String>
}
impl ConfigEnvironment {
pub fn backend(&self) -> anyhow::Result<Box<dyn DatabaseBackend>> {
Ok(Box::new(PgBackend::new(&self.connection, &self.schema_name)?))
}
}
#[derive(Debug, Clone)]
pub enum ConnectionInfo {
Url(Option<String>),
Params {
user: Option<String>,
password: Option<String>,
dbname: Option<String>,
options: Option<String>,
host: Option<String>,
port: Option<String>,
}
}