use crate::YahooError;
use crate::builder::YahooConnectorBuilder;
use crate::enterprise::EnterpriseConfig;
use crate::{
circuit_breaker::CircuitBreakerConfig, connection_pool::ConnectionPoolConfig,
observability::ObservabilityConfig, rate_limiter::RateLimitConfig,
request_deduplication::DeduplicationConfig, response_cache::ResponseCacheConfig,
retry::RetryConfig,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresetConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub rate_limit: f64,
pub circuit_breaker_threshold: u32,
pub circuit_breaker_window_secs: u64,
pub circuit_breaker_timeout_secs: u64,
pub retry_attempts: u32,
pub retry_initial_delay_ms: u64,
pub retry_max_delay_ms: u64,
pub timeout_secs: u64,
pub cache_size: usize,
pub cache_duration_secs: u64,
pub connection_pool_max: usize,
pub verbose_logging: bool,
pub enable_metrics: bool,
pub enable_tracing: bool,
}
impl From<&YahooConnectorBuilder> for PresetConfig {
fn from(builder: &YahooConnectorBuilder) -> Self {
Self {
name: "custom".to_string(),
description: None,
rate_limit: builder.rate_limit,
circuit_breaker_threshold: builder.circuit_breaker_threshold,
circuit_breaker_window_secs: builder.circuit_breaker_window_secs,
circuit_breaker_timeout_secs: builder.circuit_breaker_timeout_secs,
retry_attempts: builder.retry_attempts,
retry_initial_delay_ms: builder.retry_initial_delay_ms,
retry_max_delay_ms: builder.retry_max_delay_ms,
timeout_secs: builder.timeout.as_secs(),
cache_size: builder.cache_size,
cache_duration_secs: builder.cache_duration_secs,
connection_pool_max: builder.connection_pool_max,
verbose_logging: builder.verbose_logging,
enable_metrics: builder.enable_metrics,
enable_tracing: builder.enable_tracing,
}
}
}
impl From<PresetConfig> for YahooConnectorBuilder {
fn from(preset: PresetConfig) -> Self {
Self {
rate_limit: preset.rate_limit,
circuit_breaker_threshold: preset.circuit_breaker_threshold,
circuit_breaker_window_secs: preset.circuit_breaker_window_secs,
circuit_breaker_timeout_secs: preset.circuit_breaker_timeout_secs,
retry_attempts: preset.retry_attempts,
retry_initial_delay_ms: preset.retry_initial_delay_ms,
retry_max_delay_ms: preset.retry_max_delay_ms,
timeout: Duration::from_secs(preset.timeout_secs),
cache_size: preset.cache_size,
cache_duration_secs: preset.cache_duration_secs,
connection_pool_max: preset.connection_pool_max,
verbose_logging: preset.verbose_logging,
enable_metrics: preset.enable_metrics,
enable_tracing: preset.enable_tracing,
}
}
}
impl From<PresetConfig> for EnterpriseConfig {
fn from(preset: PresetConfig) -> Self {
Self {
circuit_breaker: CircuitBreakerConfig {
failure_threshold: preset.circuit_breaker_threshold,
success_threshold: 3, recovery_timeout_ms: preset.circuit_breaker_timeout_secs * 1000, half_open_max_requests: 3, failure_rate_window_ms: preset.circuit_breaker_window_secs * 1000, minimum_request_volume: 10, categorize_failures: true, },
retry: RetryConfig {
max_attempts: preset.retry_attempts,
base_delay_ms: preset.retry_initial_delay_ms,
max_delay_ms: preset.retry_max_delay_ms,
backoff_multiplier: 2.0, jitter_factor: 0.1, enable_exponential_backoff: true,
respect_error_categories: true,
},
deduplication: DeduplicationConfig {
cache_ttl_ms: preset.cache_duration_secs * 1000, max_cache_entries: preset.cache_size,
deduplicate_in_flight: true,
cache_successes: true,
cache_failures: true,
failure_cache_ttl_ms: 30_000, max_key_length: 256,
},
response_cache: ResponseCacheConfig {
max_entries: preset.cache_size,
default_ttl_ms: preset.cache_duration_secs * 1000, quote_ttl_ms: (preset.cache_duration_secs / 5) * 1000, search_ttl_ms: preset.cache_duration_secs * 1000,
history_ttl_ms: preset.cache_duration_secs * 1000,
max_memory_bytes: 50 * 1024 * 1024, enable_size_eviction: true,
cleanup_interval_ms: 60_000, cache_errors: false, error_ttl_ms: 5_000, },
observability: ObservabilityConfig {
enable_logging: true, log_level: if preset.verbose_logging {
crate::observability::LogLevel::Debug
} else {
crate::observability::LogLevel::Info
},
enable_metrics: preset.enable_metrics,
enable_tracing: preset.enable_tracing,
enable_health_checks: true,
metrics_interval_ms: 60_000, log_request_details: preset.verbose_logging,
log_error_details: true, slow_request_threshold_ms: preset.timeout_secs * 1000, },
connection_pool: ConnectionPoolConfig {
max_connections_per_host: preset.connection_pool_max / 2, max_total_connections: preset.connection_pool_max,
connect_timeout_ms: preset.timeout_secs * 1000, request_timeout_ms: preset.timeout_secs * 1000, keep_alive_timeout_ms: 90_000, idle_timeout_ms: 60_000, enable_http2: true,
enable_connection_reuse: true,
cleanup_interval_ms: 300_000, user_agent: "EEYF/0.1.0 (Enterprise)".to_string(),
enable_tcp_keepalive: true,
tcp_keepalive_interval_ms: 30_000, },
rate_limiter: RateLimitConfig {
requests_per_hour: (preset.rate_limit * 3600.0) as u32, burst_limit: (preset.rate_limit * 2.0).max(1.0) as u32, min_interval: Duration::from_millis((1000.0 / preset.rate_limit.max(1.0)) as u64),
},
enable_all_features: true, }
}
}
pub struct PresetManager {
builtins: HashMap<String, PresetConfig>,
user_presets_dir: Option<PathBuf>,
project_presets_dir: Option<PathBuf>,
}
impl PresetManager {
pub fn new() -> Self {
let mut builtins = HashMap::new();
let production = PresetConfig::from(&YahooConnectorBuilder::production());
builtins.insert("production".to_string(), production);
let development = PresetConfig::from(&YahooConnectorBuilder::default());
builtins.insert("development".to_string(), development);
let enterprise = PresetConfig::from(&YahooConnectorBuilder::enterprise());
builtins.insert("enterprise".to_string(), enterprise);
let minimal = PresetConfig::from(&YahooConnectorBuilder::minimal());
builtins.insert("minimal".to_string(), minimal);
Self {
builtins,
user_presets_dir: Self::get_user_presets_dir(),
project_presets_dir: Self::get_project_presets_dir(),
}
}
fn get_user_presets_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
std::env::var("APPDATA")
.ok()
.map(|appdata| PathBuf::from(appdata).join("eeyf").join("presets"))
}
#[cfg(not(target_os = "windows"))]
{
std::env::var("HOME").ok().map(|home| {
PathBuf::from(home)
.join(".config")
.join("eeyf")
.join("presets")
})
}
}
fn get_project_presets_dir() -> Option<PathBuf> {
std::env::current_dir()
.ok()
.map(|cwd| cwd.join(".eeyf").join("presets"))
}
pub fn load_preset(&self, name: &str) -> Result<PresetConfig, YahooError> {
if let Some(preset) = self.builtins.get(name) {
return Ok(preset.clone());
}
if let Some(ref dir) = self.project_presets_dir {
if let Ok(preset) = self.load_preset_from_dir(dir, name) {
return Ok(preset);
}
}
if let Some(ref dir) = self.user_presets_dir {
if let Ok(preset) = self.load_preset_from_dir(dir, name) {
return Ok(preset);
}
}
Err(YahooError::InvalidStatusCode(format!(
"Preset '{}' not found. Available built-in presets: production, development, enterprise, minimal",
name
)))
}
fn load_preset_from_dir(&self, dir: &Path, name: &str) -> Result<PresetConfig, YahooError> {
let toml_path = dir.join(format!("{}.toml", name));
let json_path = dir.join(format!("{}.json", name));
if toml_path.exists() {
let content = fs::read_to_string(&toml_path).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to read preset file: {}", e))
})?;
let preset: PresetConfig = toml::from_str(&content).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to parse TOML preset: {}", e))
})?;
return Ok(preset);
}
if json_path.exists() {
let content = fs::read_to_string(&json_path).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to read preset file: {}", e))
})?;
let preset: PresetConfig = serde_json::from_str(&content).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to parse JSON preset: {}", e))
})?;
return Ok(preset);
}
Err(YahooError::InvalidStatusCode(format!(
"Preset file not found in directory: {:?}",
dir
)))
}
pub fn save_preset(
&self,
preset: &PresetConfig,
format: PresetFormat,
) -> Result<(), YahooError> {
if self.builtins.contains_key(&preset.name) {
return Err(YahooError::InvalidStatusCode(format!(
"Cannot overwrite built-in preset '{}'",
preset.name
)));
}
let dir = self.user_presets_dir.as_ref().ok_or_else(|| {
YahooError::InvalidStatusCode("Unable to determine user presets directory".into())
})?;
fs::create_dir_all(dir).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to create presets directory: {}", e))
})?;
let (filename, content) = match format {
PresetFormat::Toml => {
let content = toml::to_string_pretty(preset).map_err(|e| {
YahooError::InvalidStatusCode(format!(
"Failed to serialize preset to TOML: {}",
e
))
})?;
(format!("{}.toml", preset.name), content)
}
PresetFormat::Json => {
let content = serde_json::to_string_pretty(preset).map_err(|e| {
YahooError::InvalidStatusCode(format!(
"Failed to serialize preset to JSON: {}",
e
))
})?;
(format!("{}.json", preset.name), content)
}
};
let path = dir.join(filename);
fs::write(&path, content).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to write preset file: {}", e))
})?;
Ok(())
}
pub fn list_presets(&self) -> Vec<String> {
let mut presets: Vec<String> = self.builtins.keys().cloned().collect();
if let Some(ref dir) = self.user_presets_dir {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
if let Some(name) = entry.path().file_stem() {
if let Some(name_str) = name.to_str() {
if !presets.contains(&name_str.to_string()) {
presets.push(name_str.to_string());
}
}
}
}
}
}
if let Some(ref dir) = self.project_presets_dir {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
if let Some(name) = entry.path().file_stem() {
if let Some(name_str) = name.to_str() {
if !presets.contains(&name_str.to_string()) {
presets.push(name_str.to_string());
}
}
}
}
}
}
presets.sort();
presets
}
}
impl Default for PresetManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub enum PresetFormat {
Toml,
Json,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preset_manager_builtins() {
let manager = PresetManager::new();
assert!(manager.load_preset("production").is_ok());
assert!(manager.load_preset("development").is_ok());
assert!(manager.load_preset("enterprise").is_ok());
assert!(manager.load_preset("minimal").is_ok());
}
#[test]
fn test_preset_manager_not_found() {
let manager = PresetManager::new();
assert!(manager.load_preset("nonexistent").is_err());
}
#[test]
fn test_preset_config_from_builder() {
let builder = YahooConnectorBuilder::production();
let preset = PresetConfig::from(&builder);
assert_eq!(preset.rate_limit, 0.5);
assert_eq!(preset.circuit_breaker_threshold, 5);
assert!(preset.enable_tracing);
}
#[test]
fn test_builder_from_preset() {
let mut preset = PresetConfig::from(&YahooConnectorBuilder::production());
preset.name = "test".to_string();
preset.rate_limit = 7.5;
preset.cache_size = 9999;
let builder = YahooConnectorBuilder::from(preset);
assert_eq!(builder.rate_limit, 7.5);
assert_eq!(builder.cache_size, 9999);
}
#[test]
fn test_list_presets() {
let manager = PresetManager::new();
let presets = manager.list_presets();
assert!(presets.contains(&"production".to_string()));
assert!(presets.contains(&"development".to_string()));
assert!(presets.contains(&"enterprise".to_string()));
assert!(presets.contains(&"minimal".to_string()));
}
#[test]
fn test_prevent_overwrite_builtin() {
let manager = PresetManager::new();
let mut preset = PresetConfig::from(&YahooConnectorBuilder::production());
preset.name = "production".to_string();
let result = manager.save_preset(&preset, PresetFormat::Toml);
assert!(result.is_err());
}
}