use super::path_resolver::ConfigEnvironment;
use super::types::Config;
use super::unified::UnifiedConfig;
use super::validation::{validate_config_file, ConfigValidationError};
use std::path::PathBuf;
mod error_types;
pub use error_types::ConfigLoadWithValidationError;
mod config_builder;
use config_builder::config_from_unified;
pub(super) use config_builder::default_config;
use config_builder::ConfigConversionResult;
mod env_overrides;
pub(super) use env_overrides::apply_env_overrides;
pub fn load_config(
) -> Result<(super::types::Config, Option<UnifiedConfig>, Vec<String>), ConfigLoadWithValidationError>
{
load_config_from_path(None)
}
pub fn load_config_from_path(
config_path: Option<&std::path::Path>,
) -> Result<(super::types::Config, Option<UnifiedConfig>, Vec<String>), ConfigLoadWithValidationError>
{
load_config_from_path_with_env(config_path, &super::path_resolver::RealConfigEnvironment)
}
#[derive(Default)]
struct GlobalLoadResult {
unified: Option<UnifiedConfig>,
content: Option<String>,
warnings: Vec<String>,
validation_errors: Vec<ConfigValidationError>,
}
fn load_global_config(
config_path: Option<&std::path::Path>,
env: &dyn ConfigEnvironment,
) -> GlobalLoadResult {
let global_config_path = config_path
.map(std::path::Path::to_path_buf)
.or_else(|| env.unified_config_path());
if let Some(path) = global_config_path.as_ref() {
if env.file_exists(path) {
let content = match env.read_file(path) {
Ok(c) => c,
Err(e) => {
return GlobalLoadResult {
unified: None,
content: None,
warnings: Vec::new(),
validation_errors: vec![ConfigValidationError::InvalidValue {
file: path.clone(),
key: "config".to_string(),
message: format!("Failed to read config file: {e}"),
}],
};
}
};
let (warnings, validation_errors) = match validate_config_file(path, &content) {
Ok(config_warnings) => (config_warnings, Vec::new()),
Err(errors) => (Vec::new(), errors),
};
let (unified, more_errors) = match UnifiedConfig::load_from_content(&content) {
Ok(cfg) => (Some(cfg), Vec::new()),
Err(e) => (
None,
vec![ConfigValidationError::InvalidValue {
file: path.clone(),
key: "config".to_string(),
message: format!("Failed to parse config: {e}"),
}],
),
};
return GlobalLoadResult {
unified,
content: Some(content),
warnings,
validation_errors: [validation_errors, more_errors].concat(),
};
} else if config_path.is_some() {
return GlobalLoadResult {
unified: None,
content: None,
warnings: vec![format!("Global config file not found: {}", path.display())],
validation_errors: Vec::new(),
};
}
}
GlobalLoadResult::default()
}
#[derive(Default)]
struct LocalLoadResult {
unified: Option<UnifiedConfig>,
content: Option<String>,
warnings: Vec<String>,
validation_errors: Vec<ConfigValidationError>,
}
fn load_local_config(env: &dyn ConfigEnvironment) -> LocalLoadResult {
if let Some(local_path) = env.local_config_path() {
if env.file_exists(&local_path) {
let content = match env.read_file(&local_path) {
Ok(c) => c,
Err(e) => {
return LocalLoadResult {
unified: None,
content: None,
warnings: Vec::new(),
validation_errors: vec![ConfigValidationError::InvalidValue {
file: local_path,
key: "config".to_string(),
message: format!("Failed to read config file: {e}"),
}],
};
}
};
let (warnings, validation_errors) = match validate_config_file(&local_path, &content) {
Ok(config_warnings) => (config_warnings, Vec::new()),
Err(errors) => (Vec::new(), errors),
};
let (unified, more_errors) = match UnifiedConfig::load_from_content(&content) {
Ok(cfg) => (Some(cfg), Vec::new()),
Err(e) => (
None,
vec![ConfigValidationError::InvalidValue {
file: local_path,
key: "config".to_string(),
message: format!("Failed to parse config: {e}"),
}],
),
};
return LocalLoadResult {
unified,
content: Some(content),
warnings,
validation_errors: [validation_errors, more_errors].concat(),
};
}
}
LocalLoadResult::default()
}
pub fn load_config_from_path_with_env(
config_path: Option<&std::path::Path>,
env: &dyn ConfigEnvironment,
) -> Result<(super::types::Config, Option<UnifiedConfig>, Vec<String>), ConfigLoadWithValidationError>
{
let global = load_global_config(config_path, env);
let local = if config_path.is_none() {
load_local_config(env)
} else {
LocalLoadResult::default()
};
let GlobalLoadResult {
unified: global_unified,
content: global_content,
warnings: global_warnings,
validation_errors: global_validation_errors,
} = global;
let LocalLoadResult {
unified: local_unified,
content: local_content,
warnings: local_warnings,
validation_errors: local_validation_errors,
} = local;
let all_validation_errors = [global_validation_errors, local_validation_errors].concat();
if !all_validation_errors.is_empty() {
return Err(ConfigLoadWithValidationError::ValidationErrors(
all_validation_errors,
));
}
let merged_unified = match (global_unified, global_content, local_unified, local_content) {
(Some(global_cfg), Some(global_raw_content), Some(local_cfg), Some(local_content)) => {
let normalized_global =
merge_global_with_built_in_agent_chain_defaults(&global_cfg, &global_raw_content);
Some(normalized_global.merge_with_content(&local_content, &local_cfg))
}
(Some(global_cfg), Some(global_raw_content), None, _) => Some(
merge_global_with_built_in_agent_chain_defaults(&global_cfg, &global_raw_content),
),
(Some(global_cfg), None, None, _) => Some(global_cfg),
(None, _, Some(local_cfg), Some(local_content)) => {
Some(UnifiedConfig::default().merge_with_content(&local_content, &local_cfg))
}
(None, _, None, _) => None,
_ => unreachable!("Unexpected config loading state"),
};
if let Some(unified_cfg) = merged_unified.as_ref() {
if let Err(message) = unified_cfg.resolve_agent_drains_checked() {
let message_str = message.to_string();
let key = if message_str.contains("references unknown chain") {
message_str
.split_whitespace()
.next()
.map_or_else(|| "agent_drains".to_string(), ToString::to_string)
} else if message_str.contains("cannot be combined") {
"agent_chain".to_string()
} else {
"agent_drains".to_string()
};
return Err(ConfigLoadWithValidationError::ValidationErrors(vec![
ConfigValidationError::InvalidValue {
file: PathBuf::from("<merged-config>"),
key,
message: message_str.clone(),
},
]));
}
}
let cloud = super::types::CloudConfig::from_env_fn(|k| env.get_env_var(k));
let conversion_result = merged_unified.as_ref().map_or_else(
|| ConfigConversionResult::new(default_config()),
config_from_unified,
);
let config = Config {
cloud,
..conversion_result.config
};
let override_result = apply_env_overrides(config, env);
let config = override_result.config;
if let Err(e) = config.cloud.validate() {
return Err(ConfigLoadWithValidationError::ValidationErrors(vec![
ConfigValidationError::InvalidValue {
file: PathBuf::from("<environment>"),
key: "cloud".to_string(),
message: e.to_string(),
},
]));
}
let all_warnings = global_warnings
.into_iter()
.chain(local_warnings)
.chain(conversion_result.warnings)
.chain(override_result.warnings)
.collect();
Ok((config, merged_unified, all_warnings))
}
fn merge_global_with_built_in_agent_chain_defaults(
global: &UnifiedConfig,
global_content: &str,
) -> UnifiedConfig {
let resolved = UnifiedConfig::default().merge_with_content(global_content, global);
UnifiedConfig {
agent_chain: resolved.agent_chain,
..global.clone()
}
}
mod env_parsing;
mod unified_config_exists;
pub use unified_config_exists::{unified_config_exists, unified_config_exists_with_env};
#[cfg(test)]
mod tests;