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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use std::{path::PathBuf, env::current_dir, fs::read_to_string};

use anyhow::anyhow;
use postgres::{Client, NoTls};
use toml_edit::{Document, ArrayOfTables, value};

use crate::meta_table::setup::setup;

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)?;

    Ok(ConfigEnvironment {
      name, is_production, connection
    })
  }).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
}

impl ConfigEnvironment {
  pub fn connect(&self) -> anyhow::Result<Client> {
    let mut client = match &self.connection {
        ConnectionInfo::Url(u) => Client::connect(u.as_ref().ok_or_else(|| anyhow!("connection string url is blank"))?, NoTls)?,
        ConnectionInfo::Params {
          user,
          password,
          dbname,
          options,
          host,
          port
        } => {
          let mut c = postgres::Config::new();
          if let Some(u) = user { c.user(u); } else { c.user(&whoami::username()); }
          if let Some(pw) = password { c.password(pw); }
          if let Some(db) = dbname { c.dbname(db); }
          if let Some(opt) = options { c.options(opt); }
          if let Some(h) = host { c.host(h); } else { c.host("localhost"); }
          if let Some(p) = port { c.port(p.parse()?); }

          c.connect(NoTls)?
        },
    };
    setup(&mut client, env!("CARGO_PKG_VERSION"))?;
    Ok(client)
  }
}

#[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>,
  }
}