use std::collections::HashMap;
use crate::{ConfigError, FloeResult};
const MAX_DEPTH: usize = 32;
pub struct VarSources<'a> {
pub profile: &'a HashMap<String, String>,
pub cli: &'a HashMap<String, String>,
pub config: &'a HashMap<String, String>,
}
pub fn resolve_vars(sources: VarSources<'_>) -> FloeResult<HashMap<String, String>> {
let raw = merge(sources);
expand_all(&raw)
}
fn merge(sources: VarSources<'_>) -> HashMap<String, String> {
let mut merged =
HashMap::with_capacity(sources.profile.len() + sources.cli.len() + sources.config.len());
merged.extend(sources.profile.iter().map(|(k, v)| (k.clone(), v.clone())));
merged.extend(sources.cli.iter().map(|(k, v)| (k.clone(), v.clone())));
merged.extend(sources.config.iter().map(|(k, v)| (k.clone(), v.clone())));
merged
}
fn expand_all(raw: &HashMap<String, String>) -> FloeResult<HashMap<String, String>> {
let mut resolved: HashMap<String, String> = HashMap::with_capacity(raw.len());
for key in raw.keys() {
if !resolved.contains_key(key.as_str()) {
let mut in_progress: Vec<String> = Vec::new();
expand_key(key, raw, &mut resolved, &mut in_progress, 0)?;
}
}
Ok(resolved)
}
fn expand_key(
key: &str,
raw: &HashMap<String, String>,
resolved: &mut HashMap<String, String>,
in_progress: &mut Vec<String>,
depth: usize,
) -> FloeResult<String> {
if let Some(v) = resolved.get(key) {
return Ok(v.clone());
}
if depth > MAX_DEPTH {
return Err(Box::new(ConfigError(format!(
"variable expansion exceeded maximum depth ({MAX_DEPTH}); \
check for deeply nested or circular references near \"${{{key}}}\""
))));
}
if in_progress.iter().any(|k| k == key) {
let chain: Vec<&str> = in_progress.iter().map(|s| s.as_str()).collect();
return Err(Box::new(ConfigError(format!(
"circular variable reference detected: {} -> {}",
chain.join(" -> "),
key
))));
}
let raw_value = raw.get(key).ok_or_else(|| {
Box::new(ConfigError(format!(
"variable \"${{{key}}}\" is referenced but not defined"
))) as Box<dyn std::error::Error + Send + Sync>
})?;
in_progress.push(key.to_string());
let expanded = expand_value(raw_value, key, raw, resolved, in_progress, depth + 1)?;
in_progress.pop();
resolved.insert(key.to_string(), expanded.clone());
Ok(expanded)
}
fn expand_value(
value: &str,
owner_key: &str,
raw: &HashMap<String, String>,
resolved: &mut HashMap<String, String>,
in_progress: &mut Vec<String>,
depth: usize,
) -> FloeResult<String> {
let mut result = String::with_capacity(value.len());
let mut rest = value;
while let Some(start) = rest.find("${") {
result.push_str(&rest[..start]);
rest = &rest[start + 2..];
let end = rest.find('}').ok_or_else(|| {
Box::new(ConfigError(format!(
"variable \"{owner_key}\": unclosed placeholder in value {value:?}"
))) as Box<dyn std::error::Error + Send + Sync>
})?;
let ref_key = rest[..end].trim();
if ref_key.is_empty() {
return Err(Box::new(ConfigError(format!(
"variable \"{owner_key}\": empty placeholder ${{}}"
))));
}
let ref_value = expand_key(ref_key, raw, resolved, in_progress, depth)?;
result.push_str(&ref_value);
rest = &rest[end + 1..];
}
result.push_str(rest);
Ok(result)
}