mod mysql;
mod redis;
pub use mysql::{MysqlConfig, MysqlConfigBuilder};
pub use redis::{RedisConfig, RedisConfigBuilder};
use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
pub trait IntoMysqlName {
fn into_name(self) -> String;
}
pub trait IntoRedisName {
fn into_name(self) -> String;
}
impl IntoMysqlName for String {
fn into_name(self) -> String {
self
}
}
impl IntoMysqlName for &str {
fn into_name(self) -> String {
self.to_string()
}
}
impl IntoRedisName for String {
fn into_name(self) -> String {
self
}
}
impl IntoRedisName for &str {
fn into_name(self) -> String {
self.to_string()
}
}
pub trait Validate {
fn validate(&self) -> anyhow::Result<()>;
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
#[serde(default)]
pub mysql: HashMap<String, MysqlConfig>,
#[serde(default)]
pub redis: HashMap<String, RedisConfig>,
}
impl Config {
pub fn mysql(&self, name: &str) -> Option<&MysqlConfig> {
self.mysql.get(name)
}
pub fn redis(&self, name: &str) -> Option<&RedisConfig> {
self.redis.get(name)
}
}
impl Validate for Config {
fn validate(&self) -> anyhow::Result<()> {
for (name, mc) in &self.mysql {
mc.validate()
.map_err(|e| anyhow::anyhow!("MySQL[{}]: {}", name, e))?;
}
for (name, rc) in &self.redis {
rc.validate()
.map_err(|e| anyhow::anyhow!("Redis[{}]: {}", name, e))?;
}
Ok(())
}
}
pub struct ConfigBuilder {
mysql: HashMap<String, MysqlConfig>,
redis: HashMap<String, RedisConfig>,
env_prefix: String,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
mysql: HashMap::new(),
redis: HashMap::new(),
env_prefix: "CC".to_string(),
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
Self::new().with_file(path)
}
pub fn from_toml(toml_str: &str) -> anyhow::Result<Self> {
Self::new().with_toml(toml_str)
}
pub fn from_env() -> anyhow::Result<Self> {
Self::new().with_env()
}
pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
self.env_prefix = prefix.into();
self
}
pub fn with_file<P: AsRef<Path>>(self, path: P) -> anyhow::Result<Self> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("读取配置文件 {} 失败: {}", path.display(), e))?;
self.with_toml(&text)
}
pub fn with_toml(self, toml_str: &str) -> anyhow::Result<Self> {
let file_cfg: Config =
toml::from_str(toml_str).map_err(|e| anyhow::anyhow!("解析 TOML 失败: {}", e))?;
Ok(self.merge(file_cfg))
}
pub fn with_env(mut self) -> anyhow::Result<Self> {
let prefix = &self.env_prefix;
self.mysql
.extend(mysql::collect_env_mysql(prefix, &self.mysql)?);
self.redis
.extend(redis::collect_env_redis(prefix, &self.redis)?);
Ok(self)
}
pub fn with_mysql(
mut self,
name: impl Into<String>,
f: impl FnOnce(MysqlConfigBuilder) -> MysqlConfigBuilder,
) -> Self {
let name = name.into();
let base = self.mysql.remove(&name).unwrap_or_default();
let cfg = f(MysqlConfigBuilder(base)).0;
self.mysql.insert(name, cfg);
self
}
pub fn with_redis(
mut self,
name: impl Into<String>,
f: impl FnOnce(RedisConfigBuilder) -> RedisConfigBuilder,
) -> Self {
let name = name.into();
let base = self.redis.remove(&name).unwrap_or_default();
let cfg = f(RedisConfigBuilder(base)).0;
self.redis.insert(name, cfg);
self
}
pub fn merge(mut self, other: Config) -> Self {
self.mysql.extend(other.mysql);
self.redis.extend(other.redis);
self
}
pub fn build(self) -> anyhow::Result<Config> {
let cfg = Config {
mysql: self.mysql,
redis: self.redis,
};
cfg.validate()?;
Ok(cfg)
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_toml() {
let toml = r#"
[mysql.default]
host = "h1"
port = 3306
user = "u1"
password = "p1"
database = "db1"
max_connections = 1
ssl_mode = "preferred"
[redis.default]
url = "redis://127.0.0.1:6379"
"#;
let cfg = ConfigBuilder::from_toml(toml).unwrap().build().unwrap();
let d = cfg.mysql("default").unwrap();
assert_eq!(d.host, "h1");
assert_eq!(d.port, 3306);
assert_eq!(cfg.redis("default").unwrap().url, "redis://127.0.0.1:6379");
}
#[test]
fn builder_basic() {
let cfg = ConfigBuilder::new()
.with_mysql("default", |m| {
m.host("10.0.0.1")
.port(3307)
.user("root")
.password("pw")
.database("test")
})
.with_redis("cache", |r| r.url("redis://localhost:6380"))
.build()
.unwrap();
assert_eq!(cfg.mysql("default").unwrap().port, 3307);
assert_eq!(cfg.redis("cache").unwrap().url, "redis://localhost:6380");
}
#[test]
fn builder_merge_file() {
let toml = r#"
[mysql.default]
host = "file-host"
user = "file-user"
password = "file-pw"
database = "file-db"
"#;
let cfg = ConfigBuilder::new()
.with_toml(toml)
.unwrap()
.build()
.unwrap();
assert_eq!(cfg.mysql("default").unwrap().host, "file-host");
}
#[test]
fn from_map_works() {
let mut mysql = HashMap::new();
mysql.insert(
"default".into(),
MysqlConfig {
host: "h".into(),
port: 3306,
user: "u".into(),
password: "p".into(),
database: "db".into(),
max_connections: 5,
ssl_mode: "preferred".into(),
disable_sql_mode: false,
},
);
let cfg = ConfigBuilder::new()
.merge(Config {
mysql,
redis: HashMap::new(),
})
.build()
.unwrap();
assert_eq!(cfg.mysql("default").unwrap().host, "h");
}
#[test]
fn env_prefix_override() {
std::env::set_var("TEST_CC_MYSQL_DEFAULT_HOST", "env-host");
std::env::set_var("TEST_CC_REDIS_DEFAULT_URL", "redis://env:6379");
let cfg = ConfigBuilder::new()
.env_prefix("TEST_CC")
.with_mysql("default", |m| m.user("u").password("p").database("db"))
.with_env()
.unwrap()
.build()
.unwrap();
assert_eq!(cfg.mysql("default").unwrap().host, "env-host");
assert_eq!(cfg.redis("default").unwrap().url, "redis://env:6379");
std::env::remove_var("TEST_CC_MYSQL_DEFAULT_HOST");
std::env::remove_var("TEST_CC_REDIS_DEFAULT_URL");
}
#[test]
fn from_file_works() {
let toml = r#"
[mysql.default]
host = "file-host"
user = "file-user"
password = "file-pw"
database = "file-db"
"#;
let cfg = ConfigBuilder::from_toml(toml).unwrap().build().unwrap();
assert_eq!(cfg.mysql("default").unwrap().host, "file-host");
}
#[test]
fn from_env_works() {
std::env::set_var("TEST_CC_MYSQL_DEFAULT_HOST", "env-host");
let cfg = ConfigBuilder::new()
.env_prefix("TEST_CC")
.with_mysql("default", |m| m.user("u").password("p").database("db"))
.with_env()
.unwrap()
.build()
.unwrap();
assert_eq!(cfg.mysql("default").unwrap().host, "env-host");
std::env::remove_var("TEST_CC_MYSQL_DEFAULT_HOST");
}
}