use crate::config::types::Configuration;
use crate::config::validator::{
validate_common_config, validate_environment_references, validate_no_circular_references,
validate_required_fields,
};
use crate::config::ConfigError;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::Path;
pub fn load_config_toml(project_path: &Path) -> Result<Configuration, ConfigError> {
let config_path = project_path.join(".stand.toml");
if !config_path.exists() {
return Err(ConfigError::ValidationError {
message: "Stand configuration not found. Run 'stand init' to initialize.".to_string(),
});
}
let content = fs::read_to_string(&config_path)?;
let mut config: Configuration = toml::from_str(&content)?;
interpolate_configuration(&mut config)?;
Ok(config)
}
pub fn load_config_toml_with_inheritance(
project_path: &Path,
) -> Result<Configuration, ConfigError> {
let mut config = load_config_toml(project_path)?;
apply_variable_inheritance(&mut config)?;
Ok(config)
}
pub fn load_config_toml_with_validation(project_path: &Path) -> Result<Configuration, ConfigError> {
let config = load_config_toml_with_inheritance(project_path)?;
crate::config::validator::validate_required_fields(&config)?;
crate::config::validator::validate_environment_references(&config)?;
crate::config::validator::validate_no_circular_references(&config)?;
crate::config::validator::validate_common_config(&config)?;
Ok(config)
}
pub fn load_config(project_path: &Path) -> Result<Configuration, ConfigError> {
let config_path = project_path.join(".stand").join("config.yaml");
if !config_path.exists() {
return Err(ConfigError::ValidationError {
message: "Stand configuration not found. Run 'stand init' to initialize.".to_string(),
});
}
let content = fs::read_to_string(&config_path)?;
let config: Configuration = serde_yaml::from_str(&content)?;
Ok(config)
}
pub fn load_config_with_validation(project_path: &Path) -> Result<Configuration, ConfigError> {
let config = load_config_basic(project_path)?;
validate_required_fields(&config)?;
validate_environment_references(&config)?;
validate_no_circular_references(&config)?;
validate_common_config(&config)?;
Ok(config)
}
pub fn load_config_with_defaults(project_path: &Path) -> Result<Configuration, ConfigError> {
let mut config = load_config_basic(project_path)?;
apply_default_values(&mut config);
Ok(config)
}
fn load_config_basic(project_path: &Path) -> Result<Configuration, ConfigError> {
let config_path = project_path.join(".stand").join("config.yaml");
if !config_path.exists() {
return Err(ConfigError::ValidationError {
message: "Stand configuration not found. Run 'stand init' to initialize.".to_string(),
});
}
let content = fs::read_to_string(&config_path)?;
let config: Configuration = serde_yaml::from_str(&content)?;
Ok(config)
}
fn apply_default_values(config: &mut Configuration) {
if config.settings.show_env_in_prompt.is_none() {
config.settings.show_env_in_prompt = Some(true);
}
for env in config.environments.values_mut() {
if env.requires_confirmation.is_none() {
env.requires_confirmation = Some(false);
}
}
}
fn interpolate_string(input: &str) -> Result<String, ConfigError> {
let mut result = String::new();
let mut chars = input.char_indices();
let input_bytes = input.as_bytes();
while let Some((i, ch)) = chars.next() {
if ch == '$' && i + 1 < input.len() && input_bytes[i + 1] == b'{' {
chars.next();
let var_start = i + 2;
let mut var_end = None;
for (pos, ch) in chars.by_ref() {
if ch == '}' {
var_end = Some(pos);
break;
}
}
let var_end = var_end.ok_or_else(|| ConfigError::ValidationError {
message: format!(
"Unterminated variable placeholder starting at position {}: missing closing '}}' for '${{...'",
i
),
})?;
let var_name = &input[var_start..var_end];
if var_name.is_empty() {
return Err(ConfigError::ValidationError {
message: format!(
"Empty variable name in placeholder at position {}: '${{}}' is not valid",
i
),
});
}
let replacement = env::var(var_name).map_err(|_| ConfigError::InterpolationError {
variable: var_name.to_string(),
})?;
result.push_str(&replacement);
} else {
result.push(ch);
}
}
Ok(result)
}
fn interpolate_configuration(config: &mut Configuration) -> Result<(), ConfigError> {
if let Some(ref mut common) = config.common {
for (_, value) in common.iter_mut() {
*value = interpolate_string(value)?;
}
}
for (_, env) in config.environments.iter_mut() {
env.description = interpolate_string(&env.description)?;
for (_, value) in env.variables.iter_mut() {
*value = interpolate_string(value)?;
}
}
Ok(())
}
fn apply_variable_inheritance(config: &mut Configuration) -> Result<(), ConfigError> {
if let Some(common) = &config.common {
let common_vars = common.clone();
for env in config.environments.values_mut() {
let mut merged_vars = common_vars.clone();
merged_vars.extend(env.variables.clone());
env.variables = merged_vars;
}
}
let mut processed = HashSet::new();
let env_names: Vec<String> = config.environments.keys().cloned().collect();
for env_name in env_names {
if !processed.contains(&env_name) {
apply_environment_inheritance(config, &env_name, &mut processed, &mut Vec::new())?;
}
}
Ok(())
}
fn apply_environment_inheritance(
config: &mut Configuration,
env_name: &str,
processed: &mut HashSet<String>,
inheritance_chain: &mut Vec<String>,
) -> Result<(), ConfigError> {
if inheritance_chain.contains(&env_name.to_string()) {
return Err(ConfigError::CircularReference {
cycle: inheritance_chain.clone(),
});
}
if processed.contains(env_name) {
return Ok(());
}
inheritance_chain.push(env_name.to_string());
let env = config.environments.get(env_name).cloned().ok_or_else(|| {
ConfigError::InvalidEnvironment {
name: env_name.to_string(),
}
})?;
if let Some(parent_name) = &env.extends {
apply_environment_inheritance(config, parent_name, processed, inheritance_chain)?;
let parent_data = config
.environments
.get(parent_name)
.map(|p| {
(
p.variables.clone(),
p.color.clone(),
p.requires_confirmation,
)
})
.unwrap_or_default();
if let Some(current_env) = config.environments.get_mut(env_name) {
let mut merged_vars = parent_data.0;
merged_vars.extend(current_env.variables.clone());
current_env.variables = merged_vars;
if current_env.color.is_none() {
current_env.color = parent_data.1;
}
if current_env.requires_confirmation.is_none() {
current_env.requires_confirmation = parent_data.2;
}
}
}
inheritance_chain.pop();
processed.insert(env_name.to_string());
Ok(())
}