use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use crate::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GouqiConfig {
pub timeout: TimeoutConfig,
pub connection_pool: ConnectionPoolConfig,
pub cache: CacheConfig,
pub metrics: MetricsConfig,
pub retry: RetryConfig,
pub rate_limiting: RateLimitingConfig,
#[cfg(any(feature = "metrics", feature = "cache"))]
pub observability: crate::observability::ObservabilityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutConfig {
#[serde(with = "humantime_serde")]
pub default: Duration,
#[serde(with = "humantime_serde")]
pub connect: Duration,
#[serde(with = "humantime_serde")]
pub read: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionPoolConfig {
pub max_connections_per_host: usize,
#[serde(with = "humantime_serde")]
pub idle_timeout: Duration,
pub http2: bool,
#[serde(with = "humantime_serde")]
pub keep_alive_timeout: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub enabled: bool,
#[serde(with = "humantime_serde")]
pub default_ttl: Duration,
pub max_entries: usize,
pub strategies: HashMap<String, CacheStrategy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStrategy {
#[serde(with = "humantime_serde")]
pub ttl: Duration,
pub cache_errors: bool,
pub use_etag: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub enabled: bool,
#[serde(with = "humantime_serde")]
pub collection_interval: Duration,
pub collect_request_times: bool,
pub collect_error_rates: bool,
pub collect_cache_stats: bool,
pub export: MetricsExportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsExportConfig {
pub format: String,
pub endpoint: Option<String>,
#[serde(with = "humantime_serde")]
pub interval: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: u32,
#[serde(with = "humantime_serde")]
pub base_delay: Duration,
#[serde(with = "humantime_serde")]
pub max_delay: Duration,
pub backoff_multiplier: f64,
pub retry_status_codes: Vec<u16>,
pub retry_on_connection_errors: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitingConfig {
pub enabled: bool,
pub requests_per_second: f64,
pub burst_capacity: u32,
pub endpoint_overrides: HashMap<String, RateLimitOverride>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitOverride {
pub requests_per_second: f64,
pub burst_capacity: u32,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
default: Duration::from_secs(30),
connect: Duration::from_secs(10),
read: Duration::from_secs(30),
}
}
}
impl Default for ConnectionPoolConfig {
fn default() -> Self {
Self {
max_connections_per_host: 10,
idle_timeout: Duration::from_secs(30),
http2: true,
keep_alive_timeout: Duration::from_secs(90),
}
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
default_ttl: Duration::from_secs(300), max_entries: 1000,
strategies: HashMap::new(),
}
}
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
enabled: true,
collection_interval: Duration::from_secs(60),
collect_request_times: true,
collect_error_rates: true,
collect_cache_stats: true,
export: MetricsExportConfig::default(),
}
}
}
impl Default for MetricsExportConfig {
fn default() -> Self {
Self {
format: "json".to_string(),
endpoint: None,
interval: Duration::from_secs(300), }
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(30),
backoff_multiplier: 2.0,
retry_status_codes: vec![429, 500, 502, 503, 504],
retry_on_connection_errors: true,
}
}
}
impl Default for RateLimitingConfig {
fn default() -> Self {
Self {
enabled: true,
requests_per_second: 10.0,
burst_capacity: 20,
endpoint_overrides: HashMap::new(),
}
}
}
impl GouqiConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let config_str = std::fs::read_to_string(&path).map_err(Error::IO)?;
let config = match path.as_ref().extension().and_then(|ext| ext.to_str()) {
Some("json") => serde_json::from_str(&config_str).map_err(Error::Serde)?,
#[cfg(feature = "yaml")]
Some("yaml") | Some("yml") => {
serde_yaml::from_str(&config_str).map_err(|e| Error::ConfigError {
message: format!("YAML parsing error: {}", e),
})?
}
#[cfg(feature = "toml-support")]
Some("toml") => toml::from_str(&config_str).map_err(|e| Error::ConfigError {
message: format!("TOML parsing error: {}", e),
})?,
Some(ext) => {
return Err(Error::ConfigError {
message: format!("Unsupported config file format: .{}", ext),
});
}
None => {
return Err(Error::ConfigError {
message: "Config file must have an extension (.json, .yaml, .toml)".to_string(),
});
}
};
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let config_str = match path.as_ref().extension().and_then(|ext| ext.to_str()) {
Some("json") => serde_json::to_string_pretty(self).map_err(Error::Serde)?,
#[cfg(feature = "yaml")]
Some("yaml") | Some("yml") => {
serde_yaml::to_string(self).map_err(|e| Error::ConfigError {
message: format!("YAML serialization error: {}", e),
})?
}
#[cfg(feature = "toml-support")]
Some("toml") => toml::to_string_pretty(self).map_err(|e| Error::ConfigError {
message: format!("TOML serialization error: {}", e),
})?,
Some(ext) => {
return Err(Error::ConfigError {
message: format!("Unsupported config file format: .{}", ext),
});
}
None => {
return Err(Error::ConfigError {
message: "Config file must have an extension (.json, .yaml, .toml)".to_string(),
});
}
};
std::fs::write(path, config_str).map_err(Error::IO)?;
Ok(())
}
pub fn merge(self, other: Self) -> Self {
Self {
timeout: other.timeout,
connection_pool: other.connection_pool,
cache: other.cache,
metrics: other.metrics,
retry: other.retry,
rate_limiting: other.rate_limiting,
#[cfg(any(feature = "metrics", feature = "cache"))]
observability: other.observability,
}
}
pub fn validate(&self) -> Result<()> {
if self.timeout.default.as_millis() == 0 {
return Err(Error::ConfigError {
message: "Default timeout must be greater than 0".to_string(),
});
}
if self.timeout.connect > self.timeout.default {
return Err(Error::ConfigError {
message: "Connect timeout cannot be greater than default timeout".to_string(),
});
}
if self.timeout.read > self.timeout.default {
return Err(Error::ConfigError {
message: "Read timeout cannot be greater than default timeout".to_string(),
});
}
if self.connection_pool.max_connections_per_host == 0 {
return Err(Error::ConfigError {
message: "Connection pool size must be greater than 0".to_string(),
});
}
if self.retry.max_attempts == 0 {
return Err(Error::ConfigError {
message: "Max retry attempts must be greater than 0".to_string(),
});
}
if self.retry.base_delay.as_millis() == 0 {
return Err(Error::ConfigError {
message: "Base retry delay must be greater than 0".to_string(),
});
}
if self.retry.max_delay < self.retry.base_delay {
return Err(Error::ConfigError {
message: "Max retry delay cannot be less than base delay".to_string(),
});
}
if self.retry.backoff_multiplier <= 0.0 {
return Err(Error::ConfigError {
message: "Backoff multiplier must be greater than 0".to_string(),
});
}
if self.cache.enabled && self.cache.max_entries == 0 {
return Err(Error::ConfigError {
message: "Cache max entries must be greater than 0 when caching is enabled"
.to_string(),
});
}
if self.cache.enabled && self.cache.default_ttl.as_millis() == 0 {
return Err(Error::ConfigError {
message: "Cache default TTL must be greater than 0 when caching is enabled"
.to_string(),
});
}
if self.rate_limiting.enabled {
if self.rate_limiting.requests_per_second <= 0.0 {
return Err(Error::ConfigError {
message: "Rate limit requests per second must be greater than 0".to_string(),
});
}
if self.rate_limiting.burst_capacity == 0 {
return Err(Error::ConfigError {
message: "Rate limit burst capacity must be greater than 0".to_string(),
});
}
}
Ok(())
}
pub fn high_throughput() -> Self {
Self {
timeout: TimeoutConfig {
default: Duration::from_secs(60),
connect: Duration::from_secs(5),
read: Duration::from_secs(60),
},
connection_pool: ConnectionPoolConfig {
max_connections_per_host: 50,
idle_timeout: Duration::from_secs(60),
http2: true,
keep_alive_timeout: Duration::from_secs(120),
},
cache: CacheConfig {
enabled: true,
default_ttl: Duration::from_secs(120),
max_entries: 5000,
strategies: HashMap::new(),
},
retry: RetryConfig {
max_attempts: 5,
base_delay: Duration::from_millis(50),
max_delay: Duration::from_secs(10),
backoff_multiplier: 1.5,
retry_status_codes: vec![429, 500, 502, 503, 504],
retry_on_connection_errors: true,
},
rate_limiting: RateLimitingConfig {
enabled: true,
requests_per_second: 50.0,
burst_capacity: 100,
endpoint_overrides: HashMap::new(),
},
..Default::default()
}
}
pub fn low_resource() -> Self {
Self {
timeout: TimeoutConfig {
default: Duration::from_secs(15),
connect: Duration::from_secs(5),
read: Duration::from_secs(15),
},
connection_pool: ConnectionPoolConfig {
max_connections_per_host: 2,
idle_timeout: Duration::from_secs(15),
http2: false,
keep_alive_timeout: Duration::from_secs(30),
},
cache: CacheConfig {
enabled: true,
default_ttl: Duration::from_secs(600),
max_entries: 100,
strategies: HashMap::new(),
},
retry: RetryConfig {
max_attempts: 2,
base_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(5),
backoff_multiplier: 2.0,
retry_status_codes: vec![429, 500, 502, 503, 504],
retry_on_connection_errors: true,
},
rate_limiting: RateLimitingConfig {
enabled: true,
requests_per_second: 2.0,
burst_capacity: 5,
endpoint_overrides: HashMap::new(),
},
metrics: MetricsConfig {
enabled: false,
..Default::default()
},
#[cfg(any(feature = "metrics", feature = "cache"))]
observability: crate::observability::ObservabilityConfig {
enable_metrics: false,
enable_caching: false,
..Default::default()
},
}
}
}