use crate::config::{loader, ConfigError};
use crate::crypto::is_encrypted;
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::path::Path;
pub fn show_environment(project_path: &Path, env_name: &str, show_values: bool) -> Result<String> {
let config_with_inheritance = loader::load_config_toml_with_inheritance(project_path)?;
let raw_config = loader::load_config_toml(project_path)?;
let env = config_with_inheritance
.environments
.get(env_name)
.ok_or_else(|| {
let mut available: Vec<_> = config_with_inheritance
.environments
.keys()
.cloned()
.collect();
available.sort();
anyhow!(
"Environment '{}' not found. Available: {}",
env_name,
available.join(", ")
)
})?;
let sources = detect_variable_sources(&raw_config, env_name).map_err(anyhow::Error::from)?;
let output = format_variables(env_name, &env.variables, &sources, show_values);
Ok(output)
}
#[derive(Debug, Clone, PartialEq)]
enum VarSource {
Local,
Inherited(String),
Common,
}
fn detect_variable_sources(
raw_config: &crate::config::types::Configuration,
env_name: &str,
) -> Result<HashMap<String, VarSource>, ConfigError> {
let mut sources = HashMap::new();
let env =
raw_config
.environments
.get(env_name)
.ok_or_else(|| ConfigError::InvalidEnvironment {
name: env_name.to_string(),
})?;
let inheritance_chain = get_inheritance_chain(raw_config, env_name)?;
let common_vars: HashMap<String, String> = raw_config.common.clone().unwrap_or_default();
let mut all_vars = HashMap::new();
all_vars.extend(common_vars.clone());
for ancestor_name in inheritance_chain.iter().rev() {
if let Some(ancestor) = raw_config.environments.get(ancestor_name) {
all_vars.extend(ancestor.variables.clone());
}
}
for var_name in all_vars.keys() {
if env.variables.contains_key(var_name) {
sources.insert(var_name.clone(), VarSource::Local);
} else {
let mut found_in_ancestor = false;
for ancestor_name in &inheritance_chain[1..] {
if let Some(ancestor) = raw_config.environments.get(ancestor_name) {
if ancestor.variables.contains_key(var_name) {
sources.insert(
var_name.clone(),
VarSource::Inherited(ancestor_name.clone()),
);
found_in_ancestor = true;
break;
}
}
}
if !found_in_ancestor && common_vars.contains_key(var_name) {
sources.insert(var_name.clone(), VarSource::Common);
}
}
}
Ok(sources)
}
fn get_inheritance_chain(
config: &crate::config::types::Configuration,
env_name: &str,
) -> Result<Vec<String>, ConfigError> {
let mut chain = Vec::new();
let mut current = env_name;
loop {
chain.push(current.to_string());
if let Some(env) = config.environments.get(current) {
if let Some(parent) = &env.extends {
current = parent;
} else {
break;
}
} else {
break;
}
}
Ok(chain)
}
fn format_variables(
env_name: &str,
variables: &HashMap<String, String>,
sources: &HashMap<String, VarSource>,
show_values: bool,
) -> String {
let mut output = String::new();
output.push_str(&format!("Environment: {}\n", env_name));
output.push_str("Variables:\n");
let mut var_names: Vec<_> = variables.keys().collect();
var_names.sort();
for var_name in var_names {
let value = &variables[var_name];
let source = sources.get(var_name).unwrap_or(&VarSource::Local);
let encrypted = is_encrypted(value);
let line = if show_values {
if encrypted {
format!(" {}=[ENCRYPTED]", var_name)
} else {
format!(" {}={}", var_name, value)
}
} else if encrypted {
format!(" {} [ENCRYPTED]", var_name)
} else {
format!(" {}", var_name)
};
let suffix = match source {
VarSource::Local => "".to_string(),
VarSource::Inherited(ancestor) => format!(" (inherited from {})", ancestor),
VarSource::Common => " (from common)".to_string(),
};
output.push_str(&format!("{}{}\n", line, suffix));
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::types::{Configuration, Environment, Settings};
use std::collections::HashMap;
fn create_test_config() -> Configuration {
let mut environments = HashMap::new();
let mut common = HashMap::new();
common.insert("APP_NAME".to_string(), "MyApp".to_string());
common.insert("LOG_FORMAT".to_string(), "json".to_string());
let mut base_vars = HashMap::new();
base_vars.insert("LOG_LEVEL".to_string(), "info".to_string());
base_vars.insert("PORT".to_string(), "3000".to_string());
let mut dev_vars = HashMap::new();
dev_vars.insert("LOG_LEVEL".to_string(), "debug".to_string());
dev_vars.insert("DEBUG".to_string(), "true".to_string());
environments.insert(
"base".to_string(),
Environment {
description: "Base environment".to_string(),
extends: None,
variables: base_vars,
color: None,
requires_confirmation: None,
},
);
environments.insert(
"dev".to_string(),
Environment {
description: "Development environment".to_string(),
extends: Some("base".to_string()),
variables: dev_vars,
color: Some("green".to_string()),
requires_confirmation: None,
},
);
Configuration {
version: "2.0".to_string(),
environments,
common: Some(common),
settings: Settings::default(),
}
}
#[test]
fn test_get_inheritance_chain() {
let config = create_test_config();
let chain = get_inheritance_chain(&config, "dev").unwrap();
assert_eq!(chain, vec!["dev", "base"]);
let chain = get_inheritance_chain(&config, "base").unwrap();
assert_eq!(chain, vec!["base"]);
}
#[test]
fn test_detect_variable_sources() {
let config = create_test_config();
let sources = detect_variable_sources(&config, "dev").unwrap();
assert_eq!(sources.get("APP_NAME"), Some(&VarSource::Common));
assert_eq!(sources.get("LOG_FORMAT"), Some(&VarSource::Common));
assert_eq!(sources.get("DEBUG"), Some(&VarSource::Local));
assert_eq!(sources.get("LOG_LEVEL"), Some(&VarSource::Local));
assert_eq!(
sources.get("PORT"),
Some(&VarSource::Inherited("base".to_string()))
);
}
#[test]
fn test_format_variables_names_only() {
let mut variables = HashMap::new();
variables.insert("APP_NAME".to_string(), "MyApp".to_string());
variables.insert("DEBUG".to_string(), "true".to_string());
let mut sources = HashMap::new();
sources.insert("APP_NAME".to_string(), VarSource::Common);
sources.insert("DEBUG".to_string(), VarSource::Local);
let output = format_variables("dev", &variables, &sources, false);
assert!(output.contains("Environment: dev"));
assert!(output.contains("Variables:"));
assert!(output.contains("APP_NAME (from common)"));
assert!(output.contains("DEBUG"));
assert!(!output.contains("DEBUG ("));
assert!(!output.contains("="));
assert!(!output.contains("MyApp"));
assert!(!output.contains("true"));
}
#[test]
fn test_format_variables_with_values() {
let mut variables = HashMap::new();
variables.insert("APP_NAME".to_string(), "MyApp".to_string());
variables.insert("DEBUG".to_string(), "true".to_string());
let mut sources = HashMap::new();
sources.insert("APP_NAME".to_string(), VarSource::Common);
sources.insert("DEBUG".to_string(), VarSource::Local);
let output = format_variables("dev", &variables, &sources, true);
assert!(output.contains("Environment: dev"));
assert!(output.contains("APP_NAME=MyApp (from common)"));
assert!(output.contains("DEBUG=true"));
assert!(!output.contains("DEBUG=true ("));
}
#[test]
fn test_format_variables_with_encrypted_values() {
let mut variables = HashMap::new();
variables.insert("API_KEY".to_string(), "encrypted:abc123".to_string());
variables.insert("DEBUG".to_string(), "true".to_string());
let mut sources = HashMap::new();
sources.insert("API_KEY".to_string(), VarSource::Local);
sources.insert("DEBUG".to_string(), VarSource::Local);
let output = format_variables("dev", &variables, &sources, true);
assert!(output.contains("API_KEY=[ENCRYPTED]"));
assert!(!output.contains("encrypted:abc123"));
assert!(output.contains("DEBUG=true"));
let output = format_variables("dev", &variables, &sources, false);
assert!(output.contains("API_KEY [ENCRYPTED]"));
assert!(output.contains("DEBUG"));
assert!(!output.contains("DEBUG [ENCRYPTED]"));
}
}