use crate::config_lib::{ConfigError, GgenConfig, Result};
use std::path::{Path, PathBuf};
pub struct ConfigLoader {
path: PathBuf,
}
impl ConfigLoader {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
if !path.exists() {
return Err(ConfigError::FileNotFound(path));
}
Ok(Self { path })
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<GgenConfig> {
let loader = Self::new(path)?;
loader.load()
}
pub fn from_str(content: &str) -> Result<GgenConfig> {
let config: GgenConfig = star_toml::from_str::<GgenConfig>(content)?;
Ok(config)
}
pub fn load(&self) -> Result<GgenConfig> {
let config = star_toml::load_file::<GgenConfig>(&self.path)?;
Ok(config)
}
pub fn find_and_load() -> Result<GgenConfig> {
let path = Self::find_config_file()?;
Self::from_file(path)
}
pub fn find_config_file() -> Result<PathBuf> {
let current = std::env::current_dir().map_err(|e| {
ConfigError::Validation(format!("Failed to get current directory: {e}"))
})?;
star_toml::find_config_file("ggen.toml", current).ok_or_else(|| {
ConfigError::FileNotFound(PathBuf::from("ggen.toml (searched all parent directories)"))
})
}
pub fn load_with_env(&self, environment: &str) -> Result<GgenConfig> {
let mut config = self.load()?;
if let Some(env_overrides) = config.env.clone() {
if let Some(overrides) = env_overrides.get(environment) {
apply_env_overrides(&mut config, overrides);
}
}
Ok(config)
}
pub fn load_with_env_from_map(
self, overrides: &[(&str, serde_json::Value)],
) -> Result<GgenConfig> {
let mut config = self.load()?;
for (_env_name, env_overrides) in overrides {
if let Some(obj) = env_overrides.as_object() {
for (key, value) in obj {
apply_single_override(&mut config, key, value);
}
}
}
Ok(config)
}
#[must_use]
pub fn path(&self) -> &Path {
self.path.as_path()
}
}
fn apply_env_overrides(config: &mut GgenConfig, overrides: &serde_json::Value) {
if let Some(obj) = overrides.as_object() {
for (key, value) in obj {
apply_single_override(config, key, value);
}
}
}
fn apply_single_override(config: &mut GgenConfig, key: &str, value: &serde_json::Value) {
let parts: Vec<&str> = key.split('.').collect();
match parts.as_slice() {
["ai", field] => {
if let Some(ai_config) = config.ai.as_mut() {
update_ai_field(ai_config, field, value);
}
}
["logging", "level"] => {
if let Some(logging) = config.logging.as_mut() {
if let Some(s) = value.as_str() {
logging.level = s.to_string();
}
}
}
["logging", field] => {
if let Some(logging) = config.logging.as_mut() {
update_logging_field(logging, field, value);
}
}
["security", field] => {
if let Some(security) = config.security.as_mut() {
update_security_field(security, field, value);
}
}
["performance", field] => {
if let Some(performance) = config.performance.as_mut() {
update_performance_field(performance, field, value);
}
}
["mcp", field] => {
if config.mcp.is_none() {
config.mcp = Some(crate::config_lib::schema::McpConfig {
name: None,
version: None,
tool_timeout_ms: default_mcp_tool_timeout(),
max_concurrent_requests: default_mcp_max_concurrent(),
transport: None,
tools: None,
zai: None,
enabled: default_mcp_enabled(),
discovery: None,
});
}
if let Some(mcp) = config.mcp.as_mut() {
update_mcp_field(mcp, field, value);
}
}
["a2a", field] => {
if config.a2a.is_none() {
config.a2a = Some(crate::config_lib::schema::A2AConfig {
agent_id: None,
agent_name: None,
agent_type: None,
transport: None,
messaging: None,
orchestration: None,
capabilities: None,
enabled: default_a2a_enabled(),
});
}
if let Some(a2a) = config.a2a.as_mut() {
update_a2a_field(a2a, field, value);
}
}
_ => {
}
}
}
fn update_ai_field(
ai: &mut crate::config_lib::schema::AiConfig, field: &str, value: &serde_json::Value,
) {
match field {
"model" => {
if let Some(s) = value.as_str() {
ai.model = s.to_string();
}
}
"temperature" => {
if let Some(f) = value.as_f64() {
ai.temperature = f as f32;
}
}
"max_tokens" => {
if let Some(n) = value.as_u64() {
ai.max_tokens = n as u32;
}
}
_ => {}
}
}
fn update_security_field(
security: &mut crate::config_lib::schema::SecurityConfig, field: &str,
value: &serde_json::Value,
) {
match field {
"require_confirmation" => {
if let Some(b) = value.as_bool() {
security.require_confirmation = b;
}
}
"audit_operations" => {
if let Some(b) = value.as_bool() {
security.audit_operations = b;
}
}
_ => {}
}
}
fn update_logging_field(
logging: &mut crate::config_lib::schema::LoggingConfig, field: &str, value: &serde_json::Value,
) {
match field {
"format" => {
if let Some(s) = value.as_str() {
logging.format = s.to_string();
}
}
"file" => {
if let Some(s) = value.as_str() {
logging.file = Some(s.to_string());
}
}
"rotation" => {
if let Some(s) = value.as_str() {
logging.rotation = Some(s.to_string());
}
}
_ => {}
}
}
fn update_performance_field(
performance: &mut crate::config_lib::schema::PerformanceConfig, field: &str,
value: &serde_json::Value,
) {
match field {
"max_workers" => {
if let Some(n) = value.as_u64() {
performance.max_workers = n as u32;
}
}
"cache_size" => {
if let Some(s) = value.as_str() {
performance.cache_size = Some(s.to_string());
}
}
"memory_limit_mb" => {
if let Some(n) = value.as_u64() {
performance.memory_limit_mb = Some(n as u32);
}
}
"parallel_execution" => {
if let Some(b) = value.as_bool() {
performance.parallel_execution = b;
}
}
_ => {}
}
}
fn update_mcp_field(
mcp: &mut crate::config_lib::schema::McpConfig, field: &str, value: &serde_json::Value,
) {
match field {
"enabled" => {
if let Some(b) = value.as_bool() {
mcp.enabled = b;
}
}
"name" => {
if let Some(s) = value.as_str() {
mcp.name = Some(s.to_string());
}
}
"version" => {
if let Some(s) = value.as_str() {
mcp.version = Some(s.to_string());
}
}
"tool_timeout_ms" => {
if let Some(n) = value.as_u64() {
mcp.tool_timeout_ms = n;
}
}
"max_concurrent_requests" => {
if let Some(n) = value.as_u64() {
mcp.max_concurrent_requests = n as usize;
}
}
_ => {}
}
}
fn update_a2a_field(
a2a: &mut crate::config_lib::schema::A2AConfig, field: &str, value: &serde_json::Value,
) {
match field {
"enabled" => {
if let Some(b) = value.as_bool() {
a2a.enabled = b;
}
}
"agent_id" => {
if let Some(s) = value.as_str() {
a2a.agent_id = Some(s.to_string());
}
}
"agent_name" => {
if let Some(s) = value.as_str() {
a2a.agent_name = Some(s.to_string());
}
}
"agent_type" => {
if let Some(s) = value.as_str() {
a2a.agent_type = Some(s.to_string());
}
}
_ => {}
}
}
const fn default_mcp_tool_timeout() -> u64 {
30000
}
const fn default_mcp_max_concurrent() -> usize {
100
}
fn default_mcp_enabled() -> bool {
false
}
fn default_a2a_enabled() -> bool {
false
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_config() {
let toml = r#"
[project]
name = "test-project"
version = "1.0.0"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert_eq!(config.project.name, "test-project");
assert_eq!(config.project.version, "1.0.0");
assert!(config.ai.is_none());
}
#[test]
fn test_parse_full_config() {
let toml = r#"
[project]
name = "full-project"
version = "2.0.0"
description = "A test project"
[ai]
provider = "openai"
model = "gpt-4"
temperature = 0.8
max_tokens = 3000
[templates]
directory = "templates"
backup_enabled = true
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert_eq!(config.project.name, "full-project");
let ai = config.ai.as_ref().unwrap();
assert_eq!(ai.provider, "openai");
assert_eq!(ai.model, "gpt-4");
assert!((ai.temperature - 0.8).abs() < f32::EPSILON);
let templates = config.templates.as_ref().unwrap();
assert_eq!(templates.directory.as_ref().unwrap(), "templates");
assert!(templates.backup_enabled);
}
#[test]
fn test_default_values() {
let toml = r#"
[project]
name = "defaults"
version = "1.0.0"
[ai]
provider = "ollama"
model = "llama2"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
let ai = config.ai.as_ref().unwrap();
assert!((ai.temperature - 0.7).abs() < f32::EPSILON);
assert_eq!(ai.max_tokens, 2000);
assert_eq!(ai.timeout, 30);
}
}