use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use serde::Deserialize;
use crate::{HealthConfig, SearchError};
#[derive(Debug, Clone, Deserialize)]
pub struct SearchConfig {
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default)]
pub health: Option<HealthEntry>,
#[serde(default, rename = "engine")]
pub engines: HashMap<String, EngineEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HealthEntry {
#[serde(default = "default_max_failures")]
pub max_failures: u32,
#[serde(default = "default_suspend_seconds")]
pub suspend_seconds: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EngineEntry {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_weight")]
pub weight: f64,
pub timeout: Option<u64>,
}
fn default_timeout() -> u64 {
10
}
fn default_max_failures() -> u32 {
3
}
fn default_suspend_seconds() -> u64 {
60
}
fn default_enabled() -> bool {
true
}
fn default_weight() -> f64 {
1.0
}
impl SearchConfig {
pub fn load(path: impl AsRef<Path>) -> crate::Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path).map_err(|e| {
SearchError::Other(format!(
"Failed to read config file {}: {}",
path.display(),
e
))
})?;
Self::parse(&content)
}
pub fn parse(content: &str) -> crate::Result<Self> {
hcl::from_str(content)
.map_err(|e| SearchError::Parse(format!("Failed to parse HCL config: {}", e)))
}
pub fn health_config(&self) -> HealthConfig {
match &self.health {
Some(h) => HealthConfig {
max_failures: h.max_failures,
suspend_duration: Duration::from_secs(h.suspend_seconds),
},
None => HealthConfig::default(),
}
}
pub fn enabled_engines(&self) -> Vec<&str> {
self.engines
.iter()
.filter(|(_, entry)| entry.enabled)
.map(|(shortcut, _)| shortcut.as_str())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_hcl_basic() {
let hcl = r#"
timeout = 10
health {
max_failures = 5
suspend_seconds = 120
}
engine "ddg" {
enabled = true
weight = 1.0
}
engine "brave" {
enabled = true
weight = 1.2
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
assert_eq!(config.timeout, 10);
let health = config.health.unwrap();
assert_eq!(health.max_failures, 5);
assert_eq!(health.suspend_seconds, 120);
assert_eq!(config.engines.len(), 2);
assert!(config.engines.contains_key("ddg"));
assert!(config.engines.contains_key("brave"));
assert_eq!(config.engines["ddg"].weight, 1.0);
assert_eq!(config.engines["brave"].weight, 1.2);
}
#[test]
fn test_parse_hcl_minimal() {
let hcl = r#"
engine "ddg" {
enabled = true
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
assert_eq!(config.timeout, 10); assert!(config.health.is_none());
assert_eq!(config.engines.len(), 1);
assert!(config.engines["ddg"].enabled);
assert_eq!(config.engines["ddg"].weight, 1.0); }
#[test]
fn test_parse_hcl_disabled_engine() {
let hcl = r#"
engine "ddg" {
enabled = false
weight = 0.5
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
assert!(!config.engines["ddg"].enabled);
assert_eq!(config.engines["ddg"].weight, 0.5);
}
#[test]
fn test_parse_hcl_engine_timeout_override() {
let hcl = r#"
timeout = 5
engine "wiki" {
enabled = true
timeout = 15
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
assert_eq!(config.timeout, 5);
assert_eq!(config.engines["wiki"].timeout, Some(15));
}
#[test]
fn test_health_config_conversion() {
let hcl = r#"
health {
max_failures = 5
suspend_seconds = 120
}
engine "ddg" {
enabled = true
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
let health_config = config.health_config();
assert_eq!(health_config.max_failures, 5);
assert_eq!(health_config.suspend_duration, Duration::from_secs(120));
}
#[test]
fn test_health_config_default_when_missing() {
let hcl = r#"
engine "ddg" {
enabled = true
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
let health_config = config.health_config();
assert_eq!(health_config.max_failures, 3);
assert_eq!(health_config.suspend_duration, Duration::from_secs(60));
}
#[test]
fn test_enabled_engines() {
let hcl = r#"
engine "ddg" {
enabled = true
}
engine "brave" {
enabled = false
}
engine "wiki" {
enabled = true
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
let enabled = config.enabled_engines();
assert_eq!(enabled.len(), 2);
assert!(enabled.contains(&"ddg"));
assert!(enabled.contains(&"wiki"));
assert!(!enabled.contains(&"brave"));
}
#[test]
fn test_invalid_hcl() {
let result = SearchConfig::parse("{{{{invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_hcl_all_engines() {
let hcl = r#"
timeout = 8
engine "ddg" {
enabled = true
weight = 1.0
}
engine "brave" {
enabled = true
weight = 1.1
}
engine "bing" {
enabled = true
weight = 1.0
}
engine "wiki" {
enabled = true
weight = 0.8
}
engine "sogou" {
enabled = false
}
engine "360" {
enabled = false
}
"#;
let config = SearchConfig::parse(hcl).unwrap();
assert_eq!(config.engines.len(), 6);
assert_eq!(config.enabled_engines().len(), 4);
}
#[test]
fn test_engine_entry_defaults() {
let hcl = r#"
engine "ddg" {}
"#;
let config = SearchConfig::parse(hcl).unwrap();
let entry = &config.engines["ddg"];
assert!(entry.enabled); assert_eq!(entry.weight, 1.0); assert!(entry.timeout.is_none());
}
#[test]
fn test_load_nonexistent_file() {
let result = SearchConfig::load("/nonexistent/path/config.hcl");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Failed to read config file"));
}
}