use anyhow::{Context, Result};
use config::{Config, Environment, File};
use dialoguer::{theme::ColorfulTheme, Confirm, Input};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenCratesConfig {
pub environment: String,
pub debug: bool,
pub name: String,
pub version: String,
pub description: String,
pub openai_api_key: String,
pub default_model: String,
pub default_output_dir: String,
pub include_tests_by_default: bool,
pub include_docs_by_default: bool,
pub database: DatabaseConfig,
pub redis: RedisConfig,
pub server: ServerConfig,
pub metrics: MetricsConfig,
pub logging: LoggingConfig,
pub tracing: TracingConfig,
pub cache: CacheConfig,
pub search: SearchConfig,
pub templates: TemplatesConfig,
#[serde(default, alias = "openai")]
pub ai: AiConfig,
pub features: FeaturesConfig,
pub security: SecurityConfig,
pub storage: StorageConfig,
pub notifications: NotificationsConfig,
pub webhooks: WebhooksConfig,
pub monitoring: MonitoringConfig,
pub experimental: ExperimentalConfig,
#[serde(default)]
pub health: HealthConfig,
pub codex: CodexConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
#[serde(default)]
pub min_connections: u32,
#[serde(default)]
pub connection_timeout: Duration,
#[serde(default)]
pub idle_timeout: Duration,
#[serde(default)]
pub max_lifetime: Duration,
pub enable_logging: bool,
#[serde(default)]
pub migration_path: String,
#[serde(default)]
pub pool_size: Option<u32>,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub run_migrations: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedisConfig {
pub url: String,
pub max_connections: u32,
pub connection_timeout: Duration,
pub response_timeout: Duration,
pub retry_attempts: u32,
pub retry_delay: Duration,
pub enable_cluster: bool,
pub cluster_nodes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub workers: usize,
pub keep_alive: u64,
pub request_timeout: u64,
pub body_limit: String,
pub enable_cors: bool,
pub enable_compression: bool,
pub enable_request_id: bool,
pub cors: CorsConfig,
pub rate_limit: RateLimitConfig,
pub tls: TlsConfig,
#[serde(default)]
pub max_connections: Option<u32>,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub cors_origins: Vec<String>,
#[serde(default)]
pub enable_swagger: bool,
#[serde(default)]
pub enable_metrics: bool,
#[serde(default)]
pub enable_health_checks: bool,
#[serde(default)]
pub enable_tracing: bool,
#[serde(default)]
pub log_level: Option<String>,
#[serde(default)]
pub max_payload_size: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorsConfig {
pub allow_origins: Vec<String>,
pub allow_methods: Vec<String>,
pub allow_headers: Vec<String>,
pub expose_headers: Vec<String>,
pub max_age: u64,
pub allow_credentials: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RateLimitConfig {
pub enabled: bool,
pub requests_per_minute: u32,
pub burst_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
pub enabled: bool,
pub cert_path: String,
pub key_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub enabled: bool,
pub port: u16,
pub path: String,
pub include_golang_metrics: bool,
pub include_process_metrics: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub enable_json: bool,
pub enable_timestamps: bool,
pub enable_file_info: bool,
pub enable_thread_ids: bool,
pub outputs: LoggingOutputs,
pub rotation: LogRotationConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingOutputs {
pub stdout: bool,
pub file: Option<String>,
pub syslog: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogRotationConfig {
pub enabled: bool,
pub max_size: String,
pub max_age: u32,
pub max_backups: u32,
pub compress: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TracingConfig {
pub enabled: bool,
pub service_name: String,
pub endpoint: String,
pub sample_rate: f64,
pub propagation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default)]
pub backend: String,
#[serde(default)]
pub capacity: Option<usize>,
#[serde(default)]
pub ttl_seconds: Option<u64>,
#[serde(default)]
pub ttl: u64,
#[serde(default)]
pub max_size: usize,
#[serde(default)]
pub eviction_policy: String,
#[serde(default)]
pub redis: RedisCacheConfig,
}
impl Default for CacheConfig {
fn default() -> Self {
CacheConfig {
backend: "memory".into(),
capacity: None,
ttl_seconds: None,
ttl: 300,
max_size: 1000,
eviction_policy: "lru".to_string(),
redis: RedisCacheConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RedisCacheConfig {
pub enabled: bool,
pub prefix: String,
pub ttl: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
pub enabled: bool,
pub provider: String,
pub timeout: u64,
pub max_results: usize,
pub safe_search: bool,
pub region: String,
pub cache: SearchCacheConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchCacheConfig {
pub enabled: bool,
pub ttl: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplatesConfig {
pub directory: String,
pub custom_directory: String,
pub cache_compiled: bool,
pub auto_reload: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AiConfig {
pub provider: String,
pub model: String,
pub temperature: f32,
pub max_tokens: u32,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub frequency_penalty: Option<f32>,
#[serde(default)]
pub presence_penalty: Option<f32>,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub max_retries: Option<u32>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub organization: Option<String>,
#[serde(default)]
pub api_version: Option<String>,
#[serde(default)]
pub retry_attempts: u32,
#[serde(default)]
pub retry_delay: u64,
#[serde(default)]
pub models: AiModelsConfig,
#[serde(default)]
pub prompts: PromptsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AiModelsConfig {
pub conceptualizer: String,
pub architect: String,
pub developer: String,
pub reviewer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PromptsConfig {
pub system_prompt: String,
pub include_examples: bool,
pub include_best_practices: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeaturesConfig {
pub enable_web_ui: bool,
pub enable_api: bool,
pub enable_cli: bool,
pub enable_webhooks: bool,
pub enable_notifications: bool,
pub enable_analytics: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub enable_auth: bool,
pub enable_api_keys: bool,
pub enable_rate_limiting: bool,
pub enable_ip_whitelist: bool,
pub ip_whitelist: Vec<String>,
pub jwt: JwtConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtConfig {
pub secret: String,
pub expiration: u64,
pub refresh_expiration: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub backend: String,
pub path: String,
pub max_file_size: String,
pub allowed_extensions: Vec<String>,
pub s3: S3Config,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S3Config {
pub enabled: bool,
pub bucket: String,
pub region: String,
pub access_key: String,
pub secret_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationsConfig {
pub enabled: bool,
pub providers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhooksConfig {
pub enabled: bool,
pub endpoints: Vec<String>,
pub timeout: u64,
pub retry_attempts: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
pub health_check_interval: u64,
pub enable_profiling: bool,
pub enable_debugging: bool,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default)]
pub interval: Option<u64>,
#[serde(default)]
pub retention: Option<u64>,
#[serde(default)]
pub alert_thresholds: Option<HashMap<String, u64>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExperimentalConfig {
pub enable_wasm_plugins: bool,
pub enable_gpu_acceleration: bool,
pub enable_distributed_cache: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthConfig {
pub check_interval: Option<u64>,
pub enabled_checks: Vec<String>,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default)]
pub timeout: Option<u64>,
}
impl Default for HealthConfig {
fn default() -> Self {
Self {
check_interval: Some(30),
enabled_checks: vec![
"database".to_string(),
"cache".to_string(),
"api".to_string(),
],
enabled: true,
endpoint: None,
timeout: Some(5),
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigManager {
config: Arc<RwLock<OpenCratesConfig>>,
config_path: PathBuf,
}
impl ConfigManager {
pub fn new(config: OpenCratesConfig) -> Result<Self> {
Ok(Self {
config: Arc::new(RwLock::new(config)),
config_path: PathBuf::from(""),
})
}
pub fn load() -> Result<Self> {
if let Ok(config) = Self::load_from_path(None) {
Ok(config)
} else {
let default_config = OpenCratesConfig::default();
let config_path = PathBuf::from("opencrates.toml");
Ok(Self {
config: Arc::new(RwLock::new(default_config)),
config_path,
})
}
}
pub fn get(&self) -> std::sync::RwLockReadGuard<'_, OpenCratesConfig> {
self.config.read().unwrap()
}
#[must_use]
pub fn get_ref(&self) -> OpenCratesConfig {
self.config.read().unwrap().clone()
}
pub fn load_from_path(config_path: Option<&Path>) -> Result<Self> {
let mut builder =
Config::builder().add_source(Config::try_from(&OpenCratesConfig::default())?);
let config_files = vec![
"config/default.toml",
"config/development.toml",
"config/production.toml",
"opencrates.toml",
];
for file in config_files {
builder = builder.add_source(File::with_name(file).required(false));
}
if let Some(path) = config_path {
builder = builder.add_source(File::from(path));
}
builder = builder.add_source(
Environment::with_prefix("OPENCRATES")
.prefix_separator("_")
.separator("__"),
);
let config = builder
.build()
.context("Failed to build configuration")?
.try_deserialize::<OpenCratesConfig>()
.context("Failed to deserialize configuration")?;
info!("Configuration loaded successfully");
debug!("Config: {:?}", config);
Ok(Self {
config: Arc::new(RwLock::new(config)),
config_path: config_path
.map(std::path::Path::to_path_buf)
.unwrap_or_default(),
})
}
pub fn config(&self) -> std::sync::RwLockReadGuard<'_, OpenCratesConfig> {
self.config.read().unwrap()
}
pub fn validate(&self) -> Result<()> {
let config = self.config();
if config.openai_api_key.is_empty() && config.ai.provider == "openai" {
return Err(anyhow::anyhow!(
"OpenAI API key is required when using OpenAI provider"
));
}
if config.database.url.is_empty() {
return Err(anyhow::anyhow!("Database URL is required"));
}
if config.server.port == 0 {
return Err(anyhow::anyhow!("Server port must be greater than 0"));
}
let template_dir = Path::new(&config.templates.directory);
if !template_dir.exists() {
return Err(anyhow::anyhow!(
"Template directory does not exist: {}",
template_dir.display()
));
}
info!("Configuration validation passed");
Ok(())
}
pub fn reload(&self) -> Result<()> {
let new_config = Self::load_from_path(Some(&self.config_path))?;
*self.config.write().unwrap() = (*new_config.config.read().unwrap()).clone();
info!("Configuration reloaded");
Ok(())
}
pub fn save_to_file(&self, path: &Path) -> Result<()> {
let toml_string = toml::to_string_pretty(&*self.config())?;
std::fs::write(path, toml_string)?;
info!("Configuration saved to {}", path.display());
Ok(())
}
pub async fn get_config(&self) -> Result<OpenCratesConfig> {
Ok(self.config().clone())
}
#[must_use]
pub fn get_config_sync(&self) -> OpenCratesConfig {
self.config().clone()
}
pub async fn set_api_key(&self, api_key: &str) -> Result<()> {
let mut config = self.config.write().unwrap();
config.openai_api_key = api_key.to_string();
Ok(())
}
pub async fn set_redis_url(&self, redis_url: &str) -> Result<()> {
let mut config = self.config.write().unwrap();
config.redis.url = redis_url.to_string();
Ok(())
}
pub async fn interactive_setup(&self) -> Result<()> {
println!("OpenCrates Configuration Setup");
let api_key: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("OpenAI API Key")
.interact_text()?;
let mut config = self.config.write().unwrap();
config.openai_api_key = api_key;
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Server host")
.default("127.0.0.1".to_string())
.interact_text()?;
config.server.host = host;
let port: u16 = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Server port")
.default(8080)
.interact()?;
config.server.port = port;
let use_redis = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Use Redis for caching?")
.default(false)
.interact()?;
if use_redis {
let redis_url: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Redis URL")
.default("redis://localhost:6379".to_string())
.interact_text()?;
config.redis.url = redis_url;
}
println!("Configuration setup complete!");
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub enum OpenCratesEnvironment {
Development,
Test,
Staging,
Production,
}
impl PartialEq for OpenCratesEnvironment {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(Self::Development, Self::Development)
| (Self::Test, Self::Test)
| (Self::Staging, Self::Staging)
| (Self::Production, Self::Production)
)
}
}
impl Eq for OpenCratesEnvironment {}
impl OpenCratesEnvironment {
#[must_use]
pub fn detect() -> Self {
match std::env::var("OPENCRATES_ENV").as_deref() {
Ok("production" | "prod") => Self::Production,
Ok("staging" | "stage") => Self::Staging,
Ok("test") => Self::Test,
_ => Self::Development,
}
}
#[must_use]
pub fn is_production(&self) -> bool {
matches!(self, Self::Production)
}
#[must_use]
pub fn is_development(&self) -> bool {
matches!(self, Self::Development)
}
}
impl std::fmt::Display for OpenCratesEnvironment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Development => write!(f, "development"),
Self::Test => write!(f, "test"),
Self::Staging => write!(f, "staging"),
Self::Production => write!(f, "production"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_default_config() {
let config = OpenCratesConfig::default();
assert_eq!(config.environment, "development");
assert_eq!(config.server.port, 8080);
assert!(config.debug);
}
#[test]
fn test_config_from_file() {
let mut file = NamedTempFile::with_suffix(".toml").unwrap();
writeln!(
file,
r#"
environment = "test"
debug = false
[server]
port = 9000
host = "0.0.0.0"
keep_alive = 60
"#
)
.unwrap();
let config = ConfigManager::load_from_path(Some(file.path())).unwrap();
assert_eq!(config.config().environment, "test");
assert_eq!(config.config().server.port, 9000);
assert!(!config.config().debug);
}
#[test]
fn test_environment_detection() {
std::env::set_var("OPENCRATES_ENV", "production");
assert_eq!(
OpenCratesEnvironment::detect(),
OpenCratesEnvironment::Production
);
assert!(OpenCratesEnvironment::detect().is_production());
std::env::set_var("OPENCRATES_ENV", "development");
assert_eq!(
OpenCratesEnvironment::detect(),
OpenCratesEnvironment::Development
);
assert!(OpenCratesEnvironment::detect().is_development());
std::env::remove_var("OPENCRATES_ENV");
assert_eq!(
OpenCratesEnvironment::detect(),
OpenCratesEnvironment::Development
);
}
}
pub type AppConfig = OpenCratesConfig;
impl OpenCratesConfig {
#[must_use]
pub fn to_openai_config(&self) -> OpenAIConfig {
OpenAIConfig {
api_key: Some(self.openai_api_key.clone()),
model: self.ai.model.clone(),
max_tokens: self.ai.max_tokens as usize,
temperature: self.ai.temperature,
top_p: self.ai.top_p,
frequency_penalty: self.ai.frequency_penalty,
presence_penalty: self.ai.presence_penalty,
timeout: self.ai.timeout,
max_retries: self.ai.max_retries,
base_url: self.ai.base_url.clone(),
organization: self.ai.organization.clone(),
api_version: self.ai.api_version.clone(),
}
}
#[must_use]
pub fn to_cache_config(&self) -> crate::utils::cache::config::CacheConfig {
crate::utils::cache::config::CacheConfig {
max_entries: self.cache.max_size,
default_ttl: Some(std::time::Duration::from_secs(self.cache.ttl)),
..crate::utils::cache::config::CacheConfig::default()
}
}
#[must_use]
pub fn to_server_config(&self) -> ServerConfig {
self.server.clone()
}
#[must_use]
pub fn to_database_config(&self) -> DatabaseConfig {
self.database.clone()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenAIConfig {
pub api_key: Option<String>,
pub model: String,
pub max_tokens: usize,
pub temperature: f32,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub frequency_penalty: Option<f32>,
#[serde(default)]
pub presence_penalty: Option<f32>,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub max_retries: Option<u32>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub organization: Option<String>,
#[serde(default)]
pub api_version: Option<String>,
}
impl OpenAIConfig {
#[must_use]
pub fn new(api_key: String) -> Self {
Self {
api_key: Some(api_key),
..Default::default()
}
}
}
impl Default for OpenAIConfig {
fn default() -> Self {
Self {
api_key: None,
model: "gpt-4".into(),
max_tokens: 256,
temperature: 0.8,
top_p: None,
frequency_penalty: None,
presence_penalty: None,
timeout: None,
max_retries: None,
base_url: None,
organization: None,
api_version: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodexConfig {
pub api_key: Option<String>,
pub api_base: String,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
}
impl Default for CodexConfig {
fn default() -> Self {
Self {
api_key: env::var("OPENAI_API_KEY").ok(),
api_base: "https://api.openai.com/v1".to_string(),
model: "gpt-4o".to_string(),
max_tokens: 4096,
temperature: 0.7,
}
}
}