libsalmo/
config.rs

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    // for now, just always use postgres
120    Ok(Box::new(PgBackend::new(&self.connection, &self.schema_name)?))
121  }
122}
123
124#[derive(Debug, Clone)]
125pub enum ConnectionInfo {
126  //
127  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}