use anyhow::{anyhow, Context, Result};
use comfy_table::{
presets::{ASCII_FULL, ASCII_MARKDOWN, UTF8_FULL, UTF8_HORIZONTAL_BORDERS_ONLY},
ContentArrangement, Table,
};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::File,
io::{Read, Write},
path::PathBuf,
process::Stdio,
str::FromStr,
};
use structopt::StructOpt;
use crate::fl;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
pub profiles: HashMap<String, Profile>,
pub table_style: TableStyle,
pub lang: Option<Lang>,
#[serde(default)]
pub arrangement: ContentArrange,
pub debug: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentArrange {
Disabled,
Dynamic,
DynamicFullWidth,
}
impl Default for ContentArrange {
fn default() -> Self {
ContentArrange::Dynamic
}
}
impl FromStr for ContentArrange {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower = s.to_ascii_lowercase();
let arr = match lower.as_str() {
"disabled" => Ok(ContentArrange::Disabled),
"dynamic" => Ok(ContentArrange::Disabled),
"dynamic-full-width" => Ok(ContentArrange::Disabled),
_ => Err(anyhow!(fl!("invalid-value", val = s))),
}?;
Ok(arr)
}
}
impl ToString for ContentArrange {
fn to_string(&self) -> String {
match self {
ContentArrange::Disabled => "disabled".to_string(),
ContentArrange::Dynamic => "dynamic".to_string(),
ContentArrange::DynamicFullWidth => "dynamic-full-width".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Lang {
#[serde(rename = "en-US")]
EnUS,
#[serde(rename = "zh-CN")]
ZhCN,
}
impl Default for Lang {
fn default() -> Self {
Lang::EnUS
}
}
impl FromStr for Lang {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower = s.to_ascii_lowercase();
if lower.starts_with("en-us") {
Ok(Lang::EnUS)
} else if lower.starts_with("zh-cn") {
Ok(Lang::ZhCN)
} else {
Err(anyhow!(fl!("invalid-value", val = s)))
}
}
}
impl ToString for Lang {
fn to_string(&self) -> String {
match self {
Lang::EnUS => "en-US",
Lang::ZhCN => "zh-CN",
}
.to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, StructOpt)]
pub struct Profile {
#[structopt(skip)]
pub name: String,
#[cfg_attr(feature = "zh-CN", doc = "数据库 hostname, IPv6地址请使用'[]'包围")]
#[cfg_attr(
feature = "en-US",
doc = "database hostname, IPv6 should be surrounded by '[]'"
)]
#[structopt(short = "h", long, default_value = "localhost")]
pub host: String,
#[cfg_attr(feature = "zh-CN", doc = "数据库 port 1 ~ 65535")]
#[cfg_attr(feature = "en-US", doc = "database port 1 ~ 65535")]
#[structopt(default_value = "3306", short = "P", long)]
pub port: u16,
#[cfg_attr(feature = "zh-CN", doc = "数据库名称")]
#[cfg_attr(feature = "en-US", doc = "database name")]
pub db: String,
#[cfg_attr(feature = "zh-CN", doc = "用户名")]
#[cfg_attr(feature = "en-US", doc = "user name")]
#[structopt(short, long)]
pub user: Option<String>,
#[cfg_attr(feature = "zh-CN", doc = "密码")]
#[cfg_attr(feature = "en-US", doc = "password")]
#[structopt(short = "pass", long)]
pub password: Option<String>,
#[cfg_attr(feature = "zh-CN", doc = "SSL 模式")]
#[cfg_attr(feature = "en-US", doc = "SSL Mode")]
#[structopt(long)]
pub ssl_mode: Option<SslMode>,
#[cfg_attr(feature = "zh-CN", doc = "SSL CA 文件路径")]
#[cfg_attr(feature = "en-US", doc = "SSL CA file path")]
#[structopt(long, parse(from_os_str))]
pub ssl_ca: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SslMode {
Disabled,
Preferred,
Required,
VerifyCa,
VerifyIdentity,
}
impl Default for SslMode {
fn default() -> Self {
SslMode::Preferred
}
}
impl FromStr for SslMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let val = match &*s.to_ascii_lowercase() {
"disabled" => SslMode::Disabled,
"preferred" => SslMode::Preferred,
"required" => SslMode::Required,
"verify_ca" => SslMode::VerifyCa,
"verify_identity" => SslMode::VerifyIdentity,
_ => return Err(anyhow!(fl!("invalid-value", val = s))),
};
Ok(val)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TableStyle {
AsciiFull,
AsciiMd,
Utf8Full,
Utf8HBorderOnly,
}
impl Default for TableStyle {
fn default() -> Self {
TableStyle::Utf8Full
}
}
impl FromStr for TableStyle {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let val = match &*s.to_ascii_lowercase() {
"asciifull" => TableStyle::AsciiFull,
"asciimd" => TableStyle::AsciiMd,
"utf8full" => TableStyle::Utf8Full,
"utf8hborderonly" => TableStyle::Utf8HBorderOnly,
_ => return Err(anyhow!(fl!("invalid-value", val = s))),
};
Ok(val)
}
}
impl Profile {
pub fn uri(&self) -> String {
let mut uri = String::from("mysql://");
if let Some(user) = &self.user {
uri.push_str(user)
}
if let Some(pass) = &self.password {
uri.push_str(&format!(":{}", pass))
}
if self.user.is_none() && self.password.is_none() {
uri.push_str(&format!("{}:{}", self.host, self.port));
} else {
uri.push_str(&format!("@{}:{}", self.host, self.port));
}
uri.push_str(&format!("/{}", self.db));
uri
}
pub fn cmd(&self, piped: bool, args: &Vec<String>) -> std::process::Command {
let mut command = std::process::Command::new("mysql");
if piped {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
}
if let Some(user) = &self.user {
command.args(&["--user", user]);
}
if let Some(pass) = &self.password {
command.arg(&format!("--password={}", pass));
}
command.args(&["--host", &self.host, "--port", &self.port.to_string()]);
command.args(&["--database", &self.db]);
command.args(args);
command
}
pub fn load_or_create_history(&self) -> Result<PathBuf> {
let mut path = PathBuf::from(std::env::var("HOME").with_context(|| fl!("home-not-set"))?);
path.push(".dcli");
path.push("history");
if !path.exists() {
std::fs::create_dir_all(&path).with_context(|| fl!("create-his-dir-failed"))?
}
path.push(format!("{}_history.txt", self.name));
if !path.exists() {
std::fs::File::create(&path)
.with_context(|| fl!("create-his-file-failed", name = self.name.clone()))?;
}
Ok(path)
}
}
impl Config {
pub fn config_path() -> Result<String> {
let home = std::env::var("HOME").with_context(|| fl!("home-not-set"))?;
let mut file = std::path::Path::new(&home).to_path_buf();
file.push(".config");
file.push("dcli.toml");
Ok(file.to_str().unwrap().to_string())
}
pub fn load() -> Result<Self> {
let path_str = Self::config_path()?;
let file = std::path::Path::new(&path_str);
if file.exists() {
let mut content = String::new();
File::open(&file)
.with_context(|| fl!("open-config-failed", file = file.to_str().unwrap_or("")))?
.read_to_string(&mut content)
.unwrap();
let config: Config =
toml::from_str(&content).with_context(|| fl!("ser-config-failed"))?;
Ok(config)
} else {
println!(
"{}",
fl!("create-config-file", file = file.to_str().unwrap())
);
let config = Self::default();
config.save()?;
Ok(config)
}
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
let mut file =
File::create(&path).with_context(|| fl!("open-config-failed", file = path))?;
let tmp_value = toml::Value::try_from(self).unwrap();
let config_str = toml::to_string_pretty(&tmp_value).unwrap();
file.write_all(config_str.as_bytes())
.with_context(|| fl!("save-config-filed"))?;
Ok(())
}
pub fn new_table(&self) -> Table {
let mut table = Table::new();
let preset = match self.table_style {
TableStyle::AsciiFull => ASCII_FULL,
TableStyle::AsciiMd => ASCII_MARKDOWN,
TableStyle::Utf8Full => UTF8_FULL,
TableStyle::Utf8HBorderOnly => UTF8_HORIZONTAL_BORDERS_ONLY,
};
table.load_preset(preset);
let arr = match self.arrangement {
ContentArrange::Disabled => ContentArrangement::Disabled,
ContentArrange::Dynamic => ContentArrangement::Dynamic,
ContentArrange::DynamicFullWidth => ContentArrangement::DynamicFullWidth,
};
table.set_content_arrangement(arr);
table
}
pub fn try_get_profile(&self, name: &str) -> Result<&Profile> {
if let Some(profile) = self.profiles.get(name) {
Ok(profile)
} else {
let mut table = self.new_table();
table.set_header(vec!["name"]);
self.profiles.keys().into_iter().for_each(|key| {
table.add_row(vec![key]);
});
let table_str = table.to_string();
Err(anyhow!(fl!(
"profile-not-found",
name = name,
table = table_str
)))
}
}
pub fn try_set_profile(&mut self, name: &str, new_profile: Profile) -> Result<()> {
self.try_get_profile(name)?;
self.profiles.insert(name.to_string(), new_profile);
Ok(())
}
}