#![allow(clippy::result_large_err)]
use super::prodigy_config::{global_config_path, project_config_path, ProdigyConfig};
use premortem::config::Config;
use premortem::prelude::*;
pub fn load_prodigy_config() -> Result<Config<ProdigyConfig>, ConfigErrors> {
load_prodigy_config_with(&RealEnv)
}
pub fn load_prodigy_config_with<E: ConfigEnv>(
env: &E,
) -> Result<Config<ProdigyConfig>, ConfigErrors> {
let global_path = global_config_path();
let project_path = project_config_path();
let mut builder = Config::<ProdigyConfig>::builder()
.source(Defaults::from(ProdigyConfig::default()))
.source(
Yaml::file(global_path.to_string_lossy().to_string())
.optional()
.named("global config"),
)
.source(
Yaml::file(project_path.to_string_lossy().to_string())
.optional()
.named("project config"),
)
.source(Env::prefix("PRODIGY__").separator("__"));
builder = builder.source(
Env::prefix("PRODIGY_")
.map("CLAUDE_API_KEY", "claude_api_key")
.map("LOG_LEVEL", "log_level")
.map("AUTO_COMMIT", "auto_commit")
.map("EDITOR", "default_editor")
.map("MAX_CONCURRENT", "max_concurrent_specs"),
);
builder.build_with_env(env)
}
pub fn load_prodigy_config_traced() -> Result<TracedConfig<ProdigyConfig>, ConfigErrors> {
load_prodigy_config_traced_with(&RealEnv)
}
pub fn load_prodigy_config_traced_with<E: ConfigEnv>(
env: &E,
) -> Result<TracedConfig<ProdigyConfig>, ConfigErrors> {
let global_path = global_config_path();
let project_path = project_config_path();
Config::<ProdigyConfig>::builder()
.source(Defaults::from(ProdigyConfig::default()))
.source(
Yaml::file(global_path.to_string_lossy().to_string())
.optional()
.named("global config"),
)
.source(
Yaml::file(project_path.to_string_lossy().to_string())
.optional()
.named("project config"),
)
.source(Env::prefix("PRODIGY__").separator("__"))
.source(
Env::prefix("PRODIGY_")
.map("CLAUDE_API_KEY", "claude_api_key")
.map("LOG_LEVEL", "log_level")
.map("AUTO_COMMIT", "auto_commit")
.map("EDITOR", "default_editor")
.map("MAX_CONCURRENT", "max_concurrent_specs"),
)
.build_traced_with_env(env)
}
#[derive(Debug, Clone, Default)]
pub struct LoadOptions {
pub config_path: Option<std::path::PathBuf>,
pub skip_global: bool,
pub skip_project: bool,
pub skip_env: bool,
}
pub fn load_prodigy_config_with_options(
options: &LoadOptions,
) -> Result<Config<ProdigyConfig>, ConfigErrors> {
load_prodigy_config_with_options_and_env(options, &RealEnv)
}
pub fn load_prodigy_config_with_options_and_env<E: ConfigEnv>(
options: &LoadOptions,
env: &E,
) -> Result<Config<ProdigyConfig>, ConfigErrors> {
let mut builder = Config::<ProdigyConfig>::builder();
builder = builder.source(Defaults::from(ProdigyConfig::default()));
if !options.skip_global {
let global_path = global_config_path();
builder = builder.source(
Yaml::file(global_path.to_string_lossy().to_string())
.optional()
.named("global config"),
);
}
if let Some(ref path) = options.config_path {
builder = builder.source(
Yaml::file(path.to_string_lossy().to_string())
.required()
.named("specified config"),
);
} else if !options.skip_project {
let project_path = project_config_path();
builder = builder.source(
Yaml::file(project_path.to_string_lossy().to_string())
.optional()
.named("project config"),
);
}
if !options.skip_env {
builder = builder
.source(Env::prefix("PRODIGY__").separator("__"))
.source(
Env::prefix("PRODIGY_")
.map("CLAUDE_API_KEY", "claude_api_key")
.map("LOG_LEVEL", "log_level")
.map("AUTO_COMMIT", "auto_commit")
.map("EDITOR", "default_editor")
.map("MAX_CONCURRENT", "max_concurrent_specs"),
);
}
builder.build_with_env(env)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_with_defaults_only() {
let env = MockEnv::new();
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "info");
assert_eq!(config.max_concurrent_specs, 4);
assert!(config.auto_commit);
}
#[test]
fn test_load_with_env_vars_only() {
#[derive(Debug, serde::Deserialize, DeriveValidate)]
struct SnakeCaseConfig {
#[validate(non_empty)]
log_level: String,
#[validate(range(1..=100))]
max_items: usize,
}
let env = MockEnv::new()
.with_file(
"config.toml",
r#"
log_level = "info"
max_items = 10
"#,
)
.with_env("APP__LOG_LEVEL", "debug")
.with_env("APP__MAX_ITEMS", "50");
let config = Config::<SnakeCaseConfig>::builder()
.source(Toml::file("config.toml"))
.source(Env::prefix("APP__").separator("__"))
.build_with_env(&env)
.expect("should load successfully");
assert_eq!(config.log_level, "debug"); assert_eq!(config.max_items, 50); }
#[test]
fn test_load_with_global_config() {
let global_path = global_config_path();
let env = MockEnv::new().with_file(
global_path.to_string_lossy().to_string(),
r#"
log_level: debug
max_concurrent_specs: 8
"#,
);
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "debug");
assert_eq!(config.max_concurrent_specs, 8);
}
#[test]
fn test_load_with_env_override() {
let global_path = global_config_path();
let env = MockEnv::new()
.with_file(global_path.to_string_lossy().to_string(), "log_level: info")
.with_env("PRODIGY__LOG_LEVEL", "debug");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "debug");
}
#[test]
fn test_load_with_project_config() {
let project_path = project_config_path();
let env = MockEnv::new().with_file(
project_path.to_string_lossy().to_string(),
r#"
project:
name: test-project
spec_dir: custom-specs
"#,
);
let config = load_prodigy_config_with(&env).unwrap();
assert!(config.project.is_some());
let project = config.project.clone().unwrap();
assert_eq!(project.name, Some("test-project".to_string()));
assert_eq!(
project.spec_dir,
Some(std::path::PathBuf::from("custom-specs"))
);
}
#[test]
fn test_load_with_options_skip_global() {
let global_path = global_config_path();
let env = MockEnv::new().with_file(
global_path.to_string_lossy().to_string(),
"log_level: debug",
);
let options = LoadOptions {
skip_global: true,
..Default::default()
};
let config = load_prodigy_config_with_options_and_env(&options, &env).unwrap();
assert_eq!(config.log_level, "info");
}
#[test]
fn test_validation_error_accumulation() {
let project_path = project_config_path();
let env = MockEnv::new().with_file(
project_path.to_string_lossy().to_string(),
r#"
log_level: ""
max_concurrent_specs: 0
storage:
compression_level: 15
"#,
);
let result = load_prodigy_config_with(&env);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors.len() >= 2,
"Expected multiple errors, got {}",
errors.len()
);
}
#[test]
fn test_missing_config_files_ok() {
let env = MockEnv::new();
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "info");
}
#[test]
fn test_layered_precedence() {
let global_path = global_config_path();
let project_path = project_config_path();
let env = MockEnv::new()
.with_file(
global_path.to_string_lossy().to_string(),
r#"
log_level: debug
max_concurrent_specs: 8
auto_commit: true
"#,
)
.with_file(
project_path.to_string_lossy().to_string(),
r#"
log_level: warn
"#,
)
.with_env("PRODIGY__LOG_LEVEL", "error");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "error"); assert_eq!(config.max_concurrent_specs, 8); assert!(config.auto_commit); }
#[test]
fn test_traced_config() {
let global_path = global_config_path();
let env = MockEnv::new()
.with_file(
global_path.to_string_lossy().to_string(),
"log_level: debug",
)
.with_env("PRODIGY__LOG_LEVEL", "warn");
let traced = load_prodigy_config_traced_with(&env).unwrap();
assert_eq!(traced.log_level, "warn");
if let Some(trace) = traced.trace("log_level") {
assert!(trace.was_overridden());
}
let config = traced.into_inner();
assert_eq!(config.log_level, "warn");
}
#[test]
fn test_legacy_env_vars_claude_api_key() {
let env = MockEnv::new().with_env("PRODIGY_CLAUDE_API_KEY", "test-api-key-123");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.claude_api_key, Some("test-api-key-123".to_string()));
assert_eq!(config.effective_api_key(), Some("test-api-key-123"));
}
#[test]
fn test_legacy_env_vars_log_level() {
let env = MockEnv::new().with_env("PRODIGY_LOG_LEVEL", "debug");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "debug");
assert_eq!(config.effective_log_level(), "debug");
}
#[test]
fn test_legacy_env_vars_auto_commit() {
let env = MockEnv::new().with_env("PRODIGY_AUTO_COMMIT", "false");
let config = load_prodigy_config_with(&env).unwrap();
assert!(!config.auto_commit);
assert!(!config.effective_auto_commit());
}
#[test]
fn test_legacy_env_vars_editor() {
let env = MockEnv::new().with_env("PRODIGY_EDITOR", "vim");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.default_editor, Some("vim".to_string()));
assert_eq!(config.effective_editor(), Some("vim"));
}
#[test]
fn test_legacy_env_vars_max_concurrent() {
let env = MockEnv::new().with_env("PRODIGY_MAX_CONCURRENT", "16");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.max_concurrent_specs, 16);
assert_eq!(config.effective_max_concurrent(), 16);
}
#[test]
fn test_legacy_env_vars_override_file() {
let global_path = global_config_path();
let env = MockEnv::new()
.with_file(
global_path.to_string_lossy().to_string(),
r#"
log_level: info
auto_commit: true
"#,
)
.with_env("PRODIGY_LOG_LEVEL", "trace")
.with_env("PRODIGY_AUTO_COMMIT", "false");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.log_level, "trace"); assert!(!config.auto_commit); }
#[test]
fn test_all_legacy_env_vars_together() {
let env = MockEnv::new()
.with_env("PRODIGY_CLAUDE_API_KEY", "my-key")
.with_env("PRODIGY_LOG_LEVEL", "warn")
.with_env("PRODIGY_AUTO_COMMIT", "true")
.with_env("PRODIGY_EDITOR", "nano")
.with_env("PRODIGY_MAX_CONCURRENT", "8");
let config = load_prodigy_config_with(&env).unwrap();
assert_eq!(config.claude_api_key, Some("my-key".to_string()));
assert_eq!(config.log_level, "warn");
assert!(config.auto_commit);
assert_eq!(config.default_editor, Some("nano".to_string()));
assert_eq!(config.max_concurrent_specs, 8);
}
}