use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Clone, Deserialize)]
pub struct IdConfig {
pub name: String,
pub length: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TargetConfig {
pub id: IdConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogConfig {
pub collect_id: String,
pub collect_id_length: usize,
pub targets: Vec<TargetConfig>,
}
impl LogConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| anyhow!("读取配置文件失败: {}", e))?;
let config: LogConfig =
serde_json::from_str(&content).map_err(|e| anyhow!("解析配置文件失败: {}", e))?;
config.validate()?;
Ok(config)
}
fn validate(&self) -> Result<()> {
if self.collect_id.len() != self.collect_id_length {
return Err(anyhow!(
"collect_id 长度不匹配: 期望 {}, 实际 {}",
self.collect_id_length,
self.collect_id.len()
));
}
let mut names = std::collections::HashSet::new();
for target in &self.targets {
if !names.insert(&target.id.name) {
return Err(anyhow!("targets 中存在重复的 name: {}", target.id.name));
}
}
Ok(())
}
pub fn get_id_config(&self, name: &str) -> Option<&IdConfig> {
self.targets
.iter()
.find(|t| t.id.name == name)
.map(|t| &t.id)
}
}
static GLOBAL_CONFIG: OnceCell<LogConfig> = OnceCell::new();
pub fn init_log_config(config_path: &Path) -> Result<()> {
let config = LogConfig::from_file(config_path)?;
GLOBAL_CONFIG
.set(config)
.map_err(|_| anyhow!("日志配置已经初始化,不能重复初始化"))?;
Ok(())
}
pub fn get_config() -> Result<&'static LogConfig> {
GLOBAL_CONFIG
.get()
.ok_or_else(|| anyhow!("日志配置未初始化,请先调用 init_log_config"))
}
pub fn validate_id(id: &str) -> Result<()> {
let config = get_config()?;
let id_config = config
.get_id_config(id)
.ok_or_else(|| anyhow!("ID '{}' 在配置中不存在", id))?;
if id.len() != id_config.length {
return Err(anyhow!(
"ID '{}' 长度不匹配: 期望 {}, 实际 {}",
id,
id_config.length,
id.len()
));
}
Ok(())
}
pub fn get_collect_id() -> Result<String> {
let config = get_config()?;
Ok(config.collect_id.clone())
}
pub fn get_collect_id_length() -> Result<usize> {
let config = get_config()?;
Ok(config.collect_id_length)
}
pub fn get_id_length(id: &str) -> Result<usize> {
let config = get_config()?;
let id_config = config
.get_id_config(id)
.ok_or_else(|| anyhow!("ID '{}' 在配置中不存在", id))?;
Ok(id_config.length)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs;
use tempfile::NamedTempFile;
#[test]
#[serial]
fn test_config_from_file() {
let config_content = r#"{
"collect_id": "RESUME-AGENT",
"collect_id_length": 12,
"targets": [
{
"id": {
"name": "user_login",
"length": 10
}
}
]
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
let config = LogConfig::from_file(path).unwrap();
assert_eq!(config.collect_id, "RESUME-AGENT");
assert_eq!(config.collect_id_length, 12);
assert_eq!(config.targets.len(), 1);
assert_eq!(config.targets[0].id.name, "user_login");
assert_eq!(config.targets[0].id.length, 10);
}
#[test]
#[serial]
fn test_config_validate_collect_id_length() {
let config_content = r#"{
"collect_id": "RESUME",
"collect_id_length": 12,
"targets": []
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
let result = LogConfig::from_file(path);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("collect_id 长度不匹配"));
}
#[test]
#[serial]
fn test_config_validate_duplicate_names() {
let config_content = r#"{
"collect_id": "RESUME-AGENT",
"collect_id_length": 12,
"targets": [
{
"id": {
"name": "user_login",
"length": 10
}
},
{
"id": {
"name": "user_login",
"length": 10
}
}
]
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
let result = LogConfig::from_file(path);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("重复的 name"));
}
#[test]
#[serial]
fn test_get_id_config() {
let config = LogConfig {
collect_id: "RESUME-AGENT".to_string(),
collect_id_length: 12,
targets: vec![TargetConfig {
id: IdConfig {
name: "user_login".to_string(),
length: 10,
},
}],
};
let id_config = config.get_id_config("user_login").unwrap();
assert_eq!(id_config.name, "user_login");
assert_eq!(id_config.length, 10);
assert!(config.get_id_config("not_exist").is_none());
}
#[test]
#[serial]
fn test_get_collect_id_length() {
let config_content = r#"{
"collect_id": "RESUME-AGENT",
"collect_id_length": 12,
"targets": [
{
"id": {
"name": "user_login",
"length": 10
}
}
]
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
init_log_config(path).unwrap();
let length = get_collect_id_length().unwrap();
assert_eq!(length, 12);
}
#[test]
#[serial]
fn test_get_id_length() {
let config_content = r#"{
"collect_id": "RESUME-AGENT",
"collect_id_length": 12,
"targets": [
{
"id": {
"name": "user_login",
"length": 10
}
}
]
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
init_log_config(path).unwrap();
let length = get_id_length("user_login").unwrap();
assert_eq!(length, 10);
}
#[test]
#[serial]
fn test_get_id_length_not_exist() {
let config_content = r#"{
"collect_id": "RESUME-AGENT",
"collect_id_length": 12,
"targets": [
{
"id": {
"name": "user_login",
"length": 10
}
}
]
}"#;
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, config_content).unwrap();
init_log_config(path).unwrap();
let result = get_id_length("not_exist");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("在配置中不存在"));
}
}