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