use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub bot: BotConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub network: NetworkConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotConfig {
pub app_id: String,
pub secret: String,
#[serde(default)]
pub sandbox: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default)]
pub log_to_file: bool,
#[serde(default = "default_log_file")]
pub log_file: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default = "default_max_reconnects")]
pub max_reconnects: u32,
#[serde(default = "default_reconnect_delay")]
pub reconnect_delay: u64,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
log_to_file: false,
log_file: default_log_file(),
}
}
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
timeout: default_timeout(),
max_reconnects: default_max_reconnects(),
reconnect_delay: default_reconnect_delay(),
}
}
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_log_file() -> String {
"bot.log".to_string()
}
fn default_timeout() -> u64 {
30
}
fn default_max_reconnects() -> u32 {
5
}
fn default_reconnect_delay() -> u64 {
5
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
let app_id =
env::var("QQ_BOT_APP_ID").map_err(|_| "QQ_BOT_APP_ID environment variable not set")?;
let secret =
env::var("QQ_BOT_SECRET").map_err(|_| "QQ_BOT_SECRET environment variable not set")?;
let sandbox = env::var("QQ_BOT_SANDBOX")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false);
Ok(Config {
bot: BotConfig {
app_id,
secret,
sandbox,
},
logging: LoggingConfig::default(),
network: NetworkConfig::default(),
})
}
pub fn load_with_fallback(
config_path: Option<&str>,
app_id: Option<String>,
secret: Option<String>,
) -> Result<Self, Box<dyn std::error::Error>> {
if let Some(path) = config_path {
if Path::new(path).exists() {
return Self::from_file(path);
}
}
for default_path in &["config.toml", "examples/config.toml"] {
if Path::new(default_path).exists() {
return Self::from_file(default_path);
}
}
if env::var("QQ_BOT_APP_ID").is_ok() && env::var("QQ_BOT_SECRET").is_ok() {
return Self::from_env();
}
if let (Some(app_id), Some(secret)) = (app_id, secret) {
return Ok(Config {
bot: BotConfig {
app_id,
secret,
sandbox: false,
},
logging: LoggingConfig::default(),
network: NetworkConfig::default(),
});
}
Err("No configuration source found. Please provide a config file, environment variables, or command line arguments.".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_config_from_toml() {
let toml_content = r#"
[bot]
app_id = "123456789"
secret = "test_secret"
sandbox = true
[logging]
level = "debug"
log_to_file = true
log_file = "test.log"
[network]
timeout = 60
max_reconnects = 10
reconnect_delay = 10
"#;
let temp_file = NamedTempFile::new().unwrap();
fs::write(temp_file.path(), toml_content).unwrap();
let config = Config::from_file(temp_file.path()).unwrap();
assert_eq!(config.bot.app_id, "123456789");
assert_eq!(config.bot.secret, "test_secret");
assert!(config.bot.sandbox);
assert_eq!(config.logging.level, "debug");
assert!(config.logging.log_to_file);
assert_eq!(config.network.timeout, 60);
}
#[test]
fn test_config_defaults() {
let logging = LoggingConfig::default();
assert_eq!(logging.level, "info");
assert!(!logging.log_to_file);
let network = NetworkConfig::default();
assert_eq!(network.timeout, 30);
assert_eq!(network.max_reconnects, 5);
}
}